slice的内部实现
slice不存储任何数据,它只描述了底层数组中的一段,它是一个包含三个字段的数据结构:
- 指针:指向slice第一个元素对应的底层数组元素的地址
- 长度:即slice中能访问的元素的个数
- 容量:即slice中第一个元素对应的底层数组元素 到 数组末尾元素的个数
可以把 slice 想象成下面的结构:
|
|
例如:对于底层数组容量是 $k$ 的 slice[i:j] 来说,长度为 $j-i$ ,容量为 $k-i$ 。
因为slice包含指向slice第一个元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。
多个slice可以共享一个底层数组,对其中一个slice的修改会反映到其他slice上。
slice的创建和初始化
一种创建 slice 的方法是内置的 make 函数,make 函数会分配一个元素为零值的底层数组并返回一个引用了它的 slice,如果只指定了长度,那么 slice 的长度和容量相等:
|
|
也可以传入第三个参数指定容量:
|
|
另一种创建 slice 的方法是 slice 字面量,和创建数组类似,只是不需要指定 $[]$ 中的值,长度和容量基于初始化给定的元素个数决定:
|
|
使用一个大于等于 len(s) 的索引访问 slice 元素会引发一个panic:
|
|
nil和空slice
一个零值的 slice 等于nil,一个为 nil 的 slice 没有底层数组,长度和容量均为 0 :
|
|
声明一个空 slice 也不会分配存储空间,长度和容量也均为 0 :
|
|
如果需要判断一个 slice 是否为空,应该使用 len(s) == 0 ,而不应该使用 s == nil 。
不管是 nil 值的 slice 还是空 slice,除了文档明确说明的地方,所有函数应该以相同的方式对待 nil 值的 slice和空 slice,对其调用 append、len 和 cap 的效果都是一样的。
slice的增长
调用函数 append 会返回一个包含修改结果的新 slice,append 总是会增加新 slice 的长度,而容量可能改变,也可能不改变,这取决于被操作的 slice 的可用容量。
如果底层数组没有足够的容量,append 会创建一个新的底层数组,然后把旧数组的值复制进来,再追加新的值。
|
|
slice的迭代
关键字 range 可以配合关键字 for 来迭代 slice 中的元素,range 会返回两个值:第一个是当前迭代到的索引位置,第二个是当前迭代到的元素的副本。range 创建了每个元素的副本,而不是直接返回对该元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。
|
|
slice的内存优化
slice 指向一个底层数组,只要 slice 一直存在内存中,其底层数组就不会被垃圾回收。假设有一个非常大的底层数组,但我们只关注其中很小的一部分,我们在这个底层数组上建立一个 slice ,然后处理这个 slice 。这时这个底层数组会一直保存在内存中,因为有一个 slice 引用了它。
深拷贝和浅拷贝:对于浅拷贝,复制出的新对象指向的地址和原对象相同,b:=a 和 b:=a[:] 都是浅拷贝;而深拷贝出的新对象指向地址和原对象不同,例如 copy 函数:
|
|
因此,上述问题的一个解决方式是:我们创建一个新的 slice,其长度和容量等于需要处理的数组长度,然后使用 copy 函数将元素复制到新 slice 中,后续使用新 slice 进行数据处理,这样原底层数组就会被回收了。
|
|
参考资料
- 《Go in Action》
- 《The Go Programming Language》