slice是切片,又称动态数组,依托数组实现,可以方便地进行扩容、传递等。
底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的slice。
数据结构
在源码包的src/runtime/slice.go:slice定义了Slice的数据结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}- array指向底层数组
- len表示切片长度
- cap表示底层数组容量 结构简单清晰
make创建slice
使用make创建slice,可以指定长度和容量,创建的时候底层会分配一个数组,数组的长度即容量。
例如slice := make([]int, 5,10)就创建了一个长度为5,容量为10的slice,只要后续添加的元素时没有达到容量10,那么就不用重新分配内存,直接使用预留内存即可。
使用数组创建slice
数组创建的slice,将与原数组共用一部分内存。
例如slice := array[5:7]
切片的长度是从数组中截取的部分,切片的容量则是从截取的位置开始到底层数组的结束。
使用数组创建切片还有一种特别的写法,即同时指定容量:
sliceA := make([]int, 5, 10)
sliceB := sliceA[0:5] // len=5,cap=10
sliceC := sliceA[0:5:5] // len=5,cap=5slice扩容
使用append向切片添加内容的时候,如果slice空间不足,就会触发slice扩容,重新申请一块更大的内存,然后将原来的slice的数据拷贝进新slice,扩容后再将数据追加进去。
扩容遵循以下原则:
- 如果原slice容量小于1024,则新slice容量将扩大为原来的2倍。
- 如果原slice容量大于等于1024,则新slice容量将扩大为原来的1.25倍。
使用append()向slice添加一个元素的流程如下:
- 如果slice容量够用,则将新元素添加进去,slice.len++,返回原slice
- 原slice容量不够,则将slice先扩容,扩容后得到新的slice
- 将新元素加入新slice,slice.len++,返回新的slice。
拷贝slice
使用内置的copy函数拷贝两个切片的时候,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。
例如,长度为10的切片拷贝到长度为5的切片时,只会拷贝5个元素。也就是说,copy过程不会发生扩容。
总结
- 每个切片都指向一个底层数组
- 每个切片都保存了当前切片的长度、底层数组的可用容量。所以len()和cap()的时间复杂度都是O(1)。
- 通过函数传递切片,只会拷贝切片这个结构体本身,不会拷贝底层数组。
- 使用append()向切片添加元素有可能触发扩容,扩容后将会生成新的切片。
使用建议
- 创建切片时尽量预留比较合适的空间,避免添加元素的过程中发生扩容的数据拷贝,有利于性能提升。
- 切片拷贝时需要判断实际拷贝的元素格式
- 谨慎使用多个切片拷贝同一个数组,以防止读写冲突。