1. Slice 定义

在 Go 的源码包中,src/runtime/slice.go 定义了 slice 的数据结构:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

其中,array 指向底层数组,len 为 slice 长度,cap 为底层数组容量。

2. Slice 多种初始化方式的异同

  1. 使用变量声明

    var s []int
    

    当采用这种方式声明一个 slice 的时候,需要注意:

    • s 是结构体 slice 的一个实例,其 fields 中 lencap 均为 0,而 arraynil
    • 若输出 s,即 fmt.Println(s),输出结果为 []
    • s == niltrue,但 &s 会显示一个有效的地址值,表明 s 已存在于内存中。
  2. 使用字面量初始化

    初始化一个空 slice:

    s := []int{}
    

    此时,slencap 也均为 0,但 array 不为 nil。虽然 fmt.Println(s) 仍会输出 [],但 s == nilfalse

  3. 使用内置函数 make() 可指定长度和空间

    s1 := make([]int, 5)		// 指定长度
    s2 := make([]int, 5, 10)	// 指定长度和空间
    
  4. 从 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 由 array arr 切取得到,s2s1 切取得到,可验证 &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() 函数时,该函数前两行执行后,s1s2lencap 均为 2 的 两个 slices,debug 发现,此时有 &s1 != &s2&s1[0] == &s2[0],说明 s1s2 是两个不同的 slice 实例,但二者的 array 值均相等。当向 s2 中加入元素 3 时,由于 s2 需要扩容,故 s2array 值被更新(重新分配了新的空间)且其 lencap 值由 2 分别更新为 34

现讨论 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, 而此时其中有效元素仅有 12。同样地,SliceRise() 中的 ss2 的拷贝,但由于 s2 底层数组中尚有空间存放新增的元素,此时 s = append(s, 0) 不会导致 array 所指空间的重新分配,即此时 ss2 共享同一底层数组 ,故在 callee SliceRise() 中修改 s 中元素便是修改 caller SlicePrint()s2 的底层数组元素。因此,caller 中 s2 底层数组的四个元素值分别更新为 2 3 4 1。但是,正是由于 pass by valuecallee 中的 s 即使通过 append()s2 的底层数组新加入了一个元素,其所更新的 len 是属于 s 的,这不会影响 caller 中 s2len,故 s2len 仍为 3,因此 s2 最后输出结果为 [2 3 4]

综上,即使有多个 slice 实例共享同一个底层数组,每个 slice 在进行 append() 操作时,只会修改其自身的 lencap,且当其需要扩容时,只会将其与共享的底层数组解引用,并为其分配新的底层数组空间。在修改共享的底层数组元素时,会影响到所有共享此底层数组的 slice。