slice是golang使用频率最高的结构之一,如果不知道它的实现原理,很容易出现”莫名其妙”的bug。
一、定义
slice是一个结构体, 定义如下
1 | type slice struct { |
slice可以看成是对数组的封装,底层数据结构是数组。slice结构体成员的含义分别为:
array:指向数组首地址的指针
len:数组中的元数个数
cap:数组总长度(容量)
二、扩容
通过append追加数据时,如果当前len==cap就会启动扩容,Go对扩容处理方式为:
- 重新申请一段更大连续空间(创建一个新数组)
- 把老slice.array中的数据拷贝到新数组中
- 创建一个新slice,把array指向新数组的首地址,设置cap值为新数组大小(容量)
- 把需要通过append添加的元素值加入到新slice.array中,并设置新slice的len为当前元数个数
源码(go1.12.7)
1 | //runtime/slice.go |
三、删除数据
和添加数据一样,删除数据也用append, 因为slice底层数据结构是数组,在数组中,删除中间元素后会把后面的数据全部往前面移,如果数组中的数据量很大,删除的数据很靠前(下标很小),这会非常低效。
golang timer 是一个基于数组的时间堆算法,堆可能是一个巨大的数组,但每次都是删除堆顶元素(数组下标为0),如果用t = append(t[:0],[t[1:]...)
删除,效率必然低下。timer源码是这样做的:详情参考
1.把要删除的元素用最后一个元素覆盖
2.缩小slice的len(len-1)
1 | last := len(tb.t) - 1 |
这种做法最后一个元素并没有被真正删除,只是len-1(通过指针方式是可以访问到)。因为slice对数据的访问是由len决定的,只有下标 < len 的数据才是有效的。但不用担心这个无效数据,下次append时,新数据会覆盖它。
四、常见问题
- 把slice作为参数或返回值
1 | func foo(arr []int){ |
情况1 len(arr) == cap(arr)
:在函数中append元素时,会重新创建一个slice,并复制数据。参数slice变量的指针指向新数组,故对原slice无影响
情况2 len(arr) < cap(arr)
:append元素时,不会生成新的slice,数据会被追加到原slice中,但是,永远读取不到。因为不管是通过下标访问,还是for range 都是只能访问到前len个元素。这时候,虽然原slice数据中的数据被append了一个元素,但len是值传递,所以原slice的len依然没变,故不能访问。但通过指针是可以取到该值的(不提倡这样做,例子只为证明推理),同理slice作为返回值也一样。如下:
1 | func callen(arr []int) int { |
- 多个切片共享底层数组
1 | func main(){ |
从上面的例子中可以看出a1,a2它们底层数组的首地址是相同的,说明a1.array和a2.array指向同一个数组,所以在修改a2元素值时,a1,a2都变化了。
- 在for range中修改slice
1 | func main(){ |
我们期待在for range中对每个slice尾部添加元素-1,但从输出结果来看,并没有达到预期。问题出在什么地方呢?
可能存在2个问题:
for range在遍历对象时,会把它的值拷贝一份,在上面的例子中 v 其实是array元素的拷贝,对它append时,可能发生了扩容。所以-1并没有append到元slice中。
v为array元素的拷贝,那么无论v的len如何变化,都不会影响原来的slice。
可以修改为:
1 | for k, _ := range array{ |
这样如果array[k] = append(array[k], -1)
等号右边的append触发了扩容,array[k]
值也是扩容后的slice。