源码包中 src/runtime/slice.go:slice 定义了 Slice 的数据结构: type slice struct {
array unsafe.Pointer
len int
cap int
}
从数据结构看Slice很清晰, array指针指向底层数组,len表示切片长度,cap表示底层数组容量。
1.2 使用make创建Slice 使用 make 来创建 Slice 时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。 例如,语句 slice := make([]int, 5, 10) 所创建的 Slice ,结构如下图所示:该 Slice 长度为 5 ,即可以使用下标 slice[0] ~ slice[4] 来操作里面的元素, capacity 为 10 ,表示后续向 slice 添加新的元素时可以不必重新分配内存,直接使用预留内存即可。 1.3 使用数组创建Slice 使用数组来创建 Slice 时, Slice 将与原数组共用一部分内存。 例如,语句 slice := array[5:7] 所创建的 Slice ,结构如下图所示: 切片从数组 array[5] 开始,到数组 array[7] 结束(不含 array[7] ),即切片长度为 2 ,数组后面的内容都作为切片的预留内存, 即capacity 为 5 。 数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方。
2、Slice 扩容 2.1、slice 扩容影响原数组 append 函数会改变 slice 所引用的数组的内容,从而影响到引用同一数组的其它 slice。
但当 slice 中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice 数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的 slice 则不受影响。
func testSlice() {
var ar = [...]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
// 打印结果:abcdefgh
fmt.Printf("%s n", ar)
slice1 := ar[2:5]
// 注:append 函数会改变 slice 所引用的数组的内容,从而影响到引用同一数组的其它 slice。
// 但当 slice 中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的
// slice 数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的 slice 则不受影响。
slice1 = append(slice1, 'A')
// 打印结果:cdeA
fmt.Printf("%s n", slice1)
// 打印结果:abcdeAgh
fmt.Printf("%s n", ar)
}
2.2、slice 扩容注意事项
使用
append
向
Slice
追加元素时,如果
Slice
空间不足,将会触发
Slice
扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新
Slice
,然后返回新
Slice
,扩容后再将数据追加进去。
例如,当向一个
capacity
为
5
,且
length
也为
5
的
Slice
再次追加
1
个元素时,就会发生扩容,如下图所示:
扩容操作只关心容量,会把原
Slice
数据拷贝到新
Slice
,追加数据由
append
在扩容结束后完成。上图可见,扩容后新的
Slice
长
度仍然是
5
,但容量由
5
提升到了
10
,原
Slice
的数据也都拷贝到了新
Slice
指向的数组中。
2.3、扩容容量的选择遵循以下规则:
1、如果原Slice
容量小于
1024
,则新
Slice
容量将扩大为原来的
2
倍;
2、如果原Slice
容量大于等于
1024
,则新
Slice
容量将扩大为原来的
1.25
倍;
2.4、使用append()向Slice添加一个元素的实现步骤如下:
1、假如Slice
容量够用,则将新元素追加进去,
Slice.len++
,返回原
Slice
2、原Slice
容量不够,则将
Slice
先扩容,扩容后得到新
Slice
3、将新元素追加进新Slice
,
Slice.len++
,返回新的
Slice
。
3、总结 1、创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能; 2、切片拷贝时需要判断实际拷贝的元素个数 3、谨慎使用多个切片操作同一个数组,以防读写冲突 4、每个切片都指向一个底层数组 5、 每个切片都保存了当前切片的长度、底层数组可用容量 6、 使用 len() 计算切片长度时间复杂度为 O(1) ,不需要遍历切片 7、使用cap() 计算切片容量时间复杂度为 O(1) ,不需要遍历切片 8、通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已 9、使用append() 向切片追加元素时有可能触发扩容,扩容后将会生成新的切片



