1. Slice 定义
在 Go 的源码包中,src/runtime/slice.go 定义了 slice 的数据结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
其中,array 指向底层数组,len 为 slice 长度,cap 为底层数组容量。
2. Slice 多种初始化方式的异同
-
使用变量声明
var s []int当采用这种方式声明一个 slice 的时候,需要注意:
s是结构体slice的一个实例,其 fields 中len和cap均为0,而array为nil;- 若输出
s,即fmt.Println(s),输出结果为[]; s == nil为true,但&s会显示一个有效的地址值,表明s已存在于内存中。
-
使用字面量初始化
初始化一个空 slice:
s := []int{}此时,
s的len和cap也均为0,但array不为nil。虽然fmt.Println(s)仍会输出[],但s == nil为false。 -
使用内置函数
make()可指定长度和空间s1 := make([]int, 5) // 指定长度 s2 := make([]int, 5, 10) // 指定长度和空间 -
从 array 或 slice 中切取
arr := [5]int{1, 2, 3, 4, 5} s1 := arr[0:2] // len(s1) == 2, cap(s1) == 5 s2 := s1[0:1] // len(s2) == 1, cap(s2) == 5在本例中,
s1由 arrayarr切取得到,s2由s1切取得到,可验证&arr == &s1[0]和&arr == &s2[0]。
3. 调用 append() 时需保持清醒
append() 扩容容量选择基于以下基本规则:
- 若
old.cap < 1024,则 slice 容量扩大为原来的 2 倍; - 若
old.cap >= 1024,则 slice 容量扩大为原来的 1.25 倍。
在使用 append() 向 slice 中加入元素时,若 slice 空间不足,即当新加入的元素会使得 len > cap 时,slice 会通过 growslice() 函数增长。值得注意的是,growslice() 函数是有返回值的,其返回值类型为 slice,故可将 slice 的增长过程认为是在内存中的另一区域开辟一块新的空间存放数据,并将这块空间的地址作为新的 array 指针,同时更新 cap,构造出 slice{p, old.len, newcap} 以返回。因此,每一次扩容理论上都会引起 slice 底层数组所在位置的变化。
一个很值得研究的例子如下:
func SliceRise(s []int) {
s = append(s, 0)
for i := range s {
s[i]++
}
}
func SlicePrint() {
s1 := []int{1, 2}
s2 := s1
s2 = append(s2, 3)
SliceRise(s1)
SliceRise(s2)
fmt.Println(s1, s2) // [1 2] [2 3 4]
}
当程序调用 SlicePrint() 函数时,该函数前两行执行后,s1 和 s2 是 len 和 cap 均为 2 的 两个 slices,debug 发现,此时有 &s1 != &s2 且 &s1[0] == &s2[0],说明 s1 和 s2 是两个不同的 slice 实例,但二者的 array 值均相等。当向 s2 中加入元素 3 时,由于 s2 需要扩容,故 s2 的 array 值被更新(重新分配了新的空间)且其 len 和 cap 值由 2 分别更新为 3 和 4。
现讨论 s1。切记,Go 语言中函数参数传递均是 pass by value,故调用 SliceRise(s1) 时,在 SliceRise() 中相当于进行了 s := s1 的操作,这与 SlicePrint() 中 s2 := s1 所造成的结果是一致的。故在 callee SliceRise() 中进行 s = append(s, 0) 操作后,s 的底层数组空间重新分配,不再代表其 caller SlicePrint() 中 s1 的底层数组,因此对其中所有元素的操作不会修改 s1 底层数组中元素的值,s1 最后的输出结果仍为 [1 2]。
s2 的情况更加隐蔽。由上可知,在调用 SliceRise(s2) 之前,s2 的底层数组容量已更新为 4, 而此时其中有效元素仅有 1 和 2。同样地,SliceRise() 中的 s 是 s2 的拷贝,但由于 s2 底层数组中尚有空间存放新增的元素,此时 s = append(s, 0) 不会导致 array 所指空间的重新分配,即此时 s 和 s2 共享同一底层数组 ,故在 callee SliceRise() 中修改 s 中元素便是修改 caller SlicePrint() 中 s2 的底层数组元素。因此,caller 中 s2 底层数组的四个元素值分别更新为 2 3 4 1。但是,正是由于 pass by value,callee 中的 s 即使通过 append() 向 s2 的底层数组新加入了一个元素,其所更新的 len 是属于 s 的,这不会影响 caller 中 s2 的 len,故 s2 的 len 仍为 3,因此 s2 最后输出结果为 [2 3 4]。
综上,即使有多个 slice 实例共享同一个底层数组,每个 slice 在进行 append() 操作时,只会修改其自身的 len 和 cap,且当其需要扩容时,只会将其与共享的底层数组解引用,并为其分配新的底层数组空间。在修改共享的底层数组元素时,会影响到所有共享此底层数组的 slice。