简介
go语言中,切片的底层是动态数组,相对长度固定的数组,使用非常广泛,犹如java界的java.util.ArrayList(都是非线程安全),但是切片在使用过程中有几个地方需要我们开发者注意。
坑1:使用函数append增加元素不要忽略返回的新切片
目前为止,go语言中除闭包函数是以引用的方式访问外部变量,其它赋值和函数传参都是传值方式处理的。不过由于切片赋值和传参的底层结构为:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
/usr/local/go/src/reflect/value.go:2760
不管切片元素多少,切片传参复制的结构很轻量,主要在于array底层是一个任意类型指针。当使用函数append添加元素时,底层数组会动态扩容,array指针会指向新的内存地址,如果我们丢弃了函数append返回的新切片,新增加的元素就不会被我们感知了。
示例:
运行结果:
切片使用函数append添加元素,导致底层数组指向新的内存区域,所以函数append返回的新切片也必须替换原切片:
运行结果:
坑2:切割操作共享底层数组导致内存泄露的风险
其实共享底层数组和java7之前String的substring实现的原理一样,只要共享底层数组都会发生内存泄露的风险。
来源:https://bugs.java.com/bugdatabase/view_bug?bug_id=4513622
示例:
运行结果:
切割生成的新切片改变元素之后,原切片也被改变,这是底层数组共享的原因,如果无意中仅使用一个很小的切片保留对非常大的不再有用的切片的引用,GC不回收大切片所占内存,就会导致内存泄露。
java7版本之后,为了解决内存泄露风险,使用数组复制解决:System.arraycopy
go切片的切割操作为了避免内存泄露,目前需要我们自己复制数据:copy函数
运行结果:
坑3:for range 循环中得到的变量也是值拷贝,改变此变量不会改变原切片
示例:
运行结果:
[1 2 3 66 88]
[1 2 3 66 88]
附:切片底层数据结构解析
上面介绍到了切片的底层数据结构为SliceHeader,我们可以使用C语言指针的方式访问切片的底层数据结构:
输出结果:
切片内容:[1 2 3]
长度:3
容量:10
C指针访问底层数组元素第一个:1
C指针访问底层数组元素第二个:2
C指针访问底层数组元素第三个:3
我们通过任意类型指针unsafe.Pointer这个桥梁,把指向切片的指针强制转换为*reflect.SliceHeader指针,这样我们就可以访问切片底层数据结构SliceHeader中的Len(切片长度)、Cap(切片容量)、Data(切片数组的指针)属性。
对于Data(切片数组的指针),我们也是通过任意类型指针unsafe.Pointer这个桥梁,以方便我们利用用C语言指针的运算规则访问底层数组的元素:
fmt.Println("C指针访问底层数组元素第一个:", *((*int)(unsafe.Pointer(p.Data))))
fmt.Println("C指针访问底层数组元素第二个:", *((*int)(unsafe.Pointer(p.Data + unsafe.Sizeof(p.Data)))))
fmt.Println("C指针访问底层数组元素第三个:", *((*int)(unsafe.Pointer(p.Data + 2*unsafe.Sizeof(p.Data)))))
小结
切片的底层数据结构让切片的传值非常轻量、高效,不像数组需要赋值所有数组元素,但是也需要我们考虑以下问题:
坑1:使用函数append增加元素不要忽略返回的新切片
坑2:切割操作共享底层数组导致内存泄露的风险
坑3:for range 循环中得到的变量也是值拷贝,改变此变量不会改变原切片