slice实现原理
slice依托数组实现,底层数组对用户屏蔽,底层数组容量不足的时候会触发扩容,具体扩容的倍数依托实际场景。
slice数据结构
type slice struct {
array unsafe.Pointer //指向底层数组
len int //表示切片长度
cap int //表示数组容量
}
创建slice的方式
用make创建slice
使用make创建的时候可以指定slice的长度和容量,同时底层会分配一个数组,数组长度为结构体中的cap,即容量。
例如:
slice := make([]int, 5, 10)
语句创建的slice结构如图:
在这个slice里,可以使用下标slice[0] ~ slice[4]来操作里面的元素,cap为10,表示后续向slice添加新的元素时可以不必重新分配内存,可以直接使用预留的内存,创建的时候预留内存,这样在添加元素的时候可以避免发生slice的扩容,但是如果预留的空间过大则会产生浪费。
用数组创建slice
如果使用数组创建slice的话,slice和原数组共用一部分内存
例如:
slice := array[4:6]
语句创建的slice结构如图:
切片从数组array[4]开始,到数组array[6]结束(不含array[6]),即创建的切片在array中的范围是左闭右开区间[4,6),所以切片长度为2,数组后面的内容都作为切片的预留内存,即cap为4。
数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方。
slice扩容
扩容触发时机
在使用append函数向slice添加元素时,如果slice可使用的空间不足,即len>=cap的时候,会触发slice的扩容。
扩容的过程
slice的扩容会重新分配一块更大的内存,将原slice中的数据拷贝到新的slice中,然后返回新的slice,再把元素添加进去。
例如,在向一个cap和len都为6的slice再添加一个元素的时候,会触发扩容,如图:
扩容操作只关心容量,而不包括追加数据的添加,即扩容只负责把原Slice数据拷贝到新Slice,追加的数据有append函数在扩容结束后的新slice中添加。如上图所示,扩容后新的slice长度没变,仍然为6,但是容量已经变为12了,原slice中的数据也被拷贝到了新的slice所指向的数组中。
扩容原则
扩容容量的选择有两种,具体遵循以下规则:
-
如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
-
如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
分析完扩容机制,那么可以看一看append函数向slice添加新元素的具体步骤:
-
如果slice还有容量,会把新的元素添加进去,然后slice.len++,返回原slice
-
如果slice容量不足,先扩容,然后得到新的slice
-
将新的元素添加进新的slice中,然后slice.len++,返回新slice
slice拷贝
使用copy函数拷贝两个切片时,会将源切片中的数据逐一拷贝到目标切片所指向的数组中,拷贝的数量是两个切片长度的最小值,也就是说假如一个长度为5的切片拷贝到长度为2的切片时,只会拷贝2个元素,所以copy的过程中不会发生扩容。
特殊的切片
根据刚才所说的,使用数组或切片生成新的切片一般使用slice := array[start:end]
的方式,这种新生成的切片并没有指定切片的容量,实际上新切片的容量是从start开始直至array的结束。
比如下面两个切片,长度和容量都是一致的,使用共同的内存地址:
sliceA := make([]int, 5, 10)
sliceB := sliceA[0:5]
另一种写法是创建新切片的同时也指定容量,即slice[start:end:cap]
, 其中cap即为新切片的容量,当然容量不能超过原切片实际值,如下所示:
sliceA := make([]int, 5, 10) //length = 5; capacity = 10
sliceB := sliceA[0:5] //length = 5; capacity = 10
sliceC := sliceA[0:5:5] //length = 5; capacity = 5
使用slice的注意事项
-
创建切片的时候可以根据实际情况预分配容量,这样可以在后续使用时避免添加元素的时候频繁地触发扩容,有助于提升性能;
-
尽量不要使用多个切片操作同一个数组,防止读写数据的冲突;
-
切片拷贝需要考虑实际拷贝元素的个数;
总结
-
切片的底层是数组,每一个切片都指向一个底层数组
-
切片实际上是一个结构体,包括三个关键字,array是当前切片所指向的数组,len是切片长度,cap是切片容量
-
使用len()计算切片长度时间复杂度为O(1),不需要遍历切片
-
使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片
-
通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体
-
使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片