最近踩了一个坑,在go语言值传递上遇到了一个问题,问题如下:
func main() {
s := []int{1,2,3,4}
s1 := s // 对象的深拷贝,将 s 赋值给 s1,s和s1的地址不一样
fmt.Printf("s 的地址:%p,s1 的地址:%p\n",&s,&s1) // s 的地址:0xc0000a6020,s1 的地址:0xc0000a6040
s1[0] = 12
s[2] = 1232
fmt.Println(s1) // [12 2 1232 4]
fmt.Println(s) // [12 2 1232 4]
}
以上问题,产生的原因很简单,:
新的 slice 仍然指向了原来的底层数组,导致后面对新slice做的修改都是在原来指向的底层数组上进行的,即两者指向的是同一个数组。
下面深度分析一下go语言的slice结构,以便于更清晰认识该问题并合理避坑。
go slice 解析
基础结构
先看看 go slice 的结构体
type slice struct {
array unsafe.Pointer // 指针,指向底层数组
len int // slice长度,即当前slice可以访问的范围
cap int // slice容量,当前slice可访问底层数组的最大范围,如果cap不够,则会执行扩容操作
}
我们看到 slice 的底层就只是一个指向数组的指针 array 而已,其实就是底层数组的地址。
这么看来,slice 用 len 来动态维护slice的大小,通过 cap 来动态维护底层数组的大小。
我画一个简图就可以清晰看到如下内容:
数组的使用,有一个很大的制约,就是数组长度很多时候无法确定,需要扩容就要手动复制到一个更长的数组上去。为了方便,slice 替我们做了这样的操作。下面看看 slice 的扩容机制。
扩容
当 cap > len,执行增加元素操作,如果 cap 满足要求就不需要执行扩容操作,只需要将数组访问范围(len)扩大即可。因为cap就是底层数组的大小,go 在声明slice时,就保留了这部分空间,即预先保留了cap大小的空间,只是slice只能访问len大小的内容,超出部分访问不了。 源码如下:
// 计算a*b的大小并判断是否会溢出
// MulUintptr returns a * b and whether the multiplication overflowed.
// On supported platforms this is an intrinsic lowered by the compiler.
func MulUintptr(a, b uintptr) (uintptr, bool) {
if a|b < 1<<(4*sys.PtrSize) || a == 0 {
return a * b, false
}
overflow := b > MaxUintptr/a
return a * b, overflow
}
创建slice
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap)) // 计算slice底层数组的基本类型的size*cap,并判断是否会溢出,即会申请一块size*cap大小的内存空间mem
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
所以,当cap满足要求时,slice就不必要执行扩容操作了。那么,什么时候会执行扩容操作呢?性能如何? 例如如下操作,底层数组的大小已经不满足要求了,所以slice就会执行扩容操作。
s := []int{1,2,3,4,5} // cap = 5
s = append(s, 10,12) // 原来的容量不满足要求,会执行扩容操作
扩容机制: 1. 如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap) 2. 如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap) 3. 如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap) 4. 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
我们可以看看具体的策略的实现:
newcap := old.cap
doublecap := newcap + newcap // 两倍扩容
if cap > doublecap { // 待扩容大小大于原切片的两倍,则按照待扩容大小处理
newcap = cap
} else {
if old.len < 1024 { // 当原切片长度小于1024时,新切片的容量会直接翻倍。
newcap = doublecap
} else { // 当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
扩容踩坑
以上扩容机制也都熟悉了,整个切片可以总结为一个可以动态扩容的指向数组的指针。概念不难,但是使用起来,经常会遇到坑,例如,我们在拷贝slice的时候,经常会只拷贝切片的地址和其包含的值,但却没有注意到新的slice其实和旧的slice指向的同一个地址。所以在拷贝slice的时候,一定要注意拷贝其底层数组。 坑1:由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。
看一个demo:
func main() {
s := make([]int, 10,15)
array := [6]int{1,2,3,4,5,6}
s = array[2:6]
// s 没有容量扩容了,新slice和s指向不同数组(可以理解为执行了slice的深拷贝)
s1 := append(s,1,2)
fmt.Printf("s 的地址:%p, s1 的地址:%p\n",&s,&s1)
fmt.Println(s)
s[1] = 123
fmt.Println(s1)
// 指向同一数组(s2还有容量可以扩容,所以其实s2和s3其实是都指向同一数组的,可以理解为浅拷贝)
s2 := make([]int, 10,15)
s2 = append(s,1,2,3,4)
s3 := append(s2, 4,5)
fmt.Println(s2)
s2[len(s2)-1] = 123
fmt.Println(s3)
fmt.Printf("s2 的地址:%p, s3 的地址:%p\n",&s2,&s3)
}
切片拷贝
func main() {
// deep copy
array := []int{1,2,3,4,5,6}
s := array[0:]
// 拷贝s
s1 := make([]int, 6)
copy(s1,s) // 有坑,要等长,不然以更小的为准
s[5] = 123
fmt.Printf("s 的地址:%p, s1 的地址:%p\n",&s,&s1)
fmt.Println(s1)
// 更优雅的深拷贝
_ = Clone(s1, s)
s[5] = 12311
fmt.Printf("s 的地址:%p, s1 的地址:%p\n",&s,&s1)
fmt.Println(s1)
// 等号复制,浅拷贝
s2 := s
fmt.Printf("s 的地址:%p, s2 的地址:%p\n",&s,&s2)
s[1] = 1111
fmt.Println(s2)
}
func Clone(a, b interface{}) error {
buff := new(bytes.Buffer)
enc := gob.NewEncoder(buff)
dec := gob.NewDecoder(buff)
if err := enc.Encode(a); err != nil {
return err
}
if err := dec.Decode(b); err != nil {
return err
}
return nil
}








网友评论