Go 切片的容量

Go 切片有很多细节的点,比如切片的 append、copy 操作,我们经常使用 append 操作,如果切片预申请容量不足的话,底层会触发 growslice 的操作,会 重新生成一个新的切片。这个过程就有可能触发两个问题:

  1. 切片是引用传递的,通过函数参数传递的切片,因为可能在函数内部触发扩容,导致切片没有按预期被修改
  2. 因为预申请的容量太小,触发频繁的 growslice,影响到系统性能

切片有两个属性,len 和 cap,cap 就和我们说的第一点有关系。有时候,我们还会只使用已有切片的一部分数据,我们可以声明多个变量,分别指向切片的不同区间。 比方说,下面的两个变量 top3 和 last3 共用 lives 的内存


	lives := make([]int, 10)
	top3 := lives[0:3]
	last3 := lives[7:]

我们通过一个反射的例子来理解,dest 和 elem 共用了底层的一份内存数据,为什么 dest 输出的结果是空呢?因为提前就把 dest 的 cap 设置成了6, 这里只 append 了一个整数1,所以,append 的时候也不会触发底层数组的扩容,那这是什么原因呢?


    func main() {
        dest := make([]int, 0, 6)
        elem := reflect.ValueOf(&dest).Elem()
        elem = reflect.Append(elem, reflect.ValueOf(1))
    
        fmt.Println(dest)               // output []
        fmt.Println(elem.Interface())   // output [1]
    }

我们调整一下代码,将等于号的赋值操作,改成反射 Set 的赋值,主要看第4行代码,之前是 = 号,现在调整成了 Set,为什么 Set 之后 dest 就被赋值了呢? 我们从 Set 方法的注释中了解到:Set assigns x to the value v,也属于一个赋值操作。

1
2
3
4
5
6
7
8
    func main() {
        dest := make([]int, 0, 6)
        elem := reflect.ValueOf(&dest).Elem()
        elem.Set(reflect.Append(elem, reflect.ValueOf(1)))
    
        fmt.Println(dest)               // output [1]
        fmt.Println(elem.Interface())   // output [1]
    }

还有一点困惑在于,难不成 dest 和 elem 底层不共用一个数组,思考到这一点,基本就已经开始怀疑人生了。我们尝试改变 dest 的值,来观察效果。 在 elem append 操作之后,我们对变量 dest 也执行 append 操作。打印的结果是两个对象都是 2。看到这个数据结果也就基本可以确定一件事情: dest 和 elem 底层共用同一内存空间,但两个切片的 len 属性不同。

1
2
3
4
5
6
7
8
9
    func main() {
        dest := make([]int, 0, 6)
        elem := reflect.ValueOf(&dest).Elem()
        elem = reflect.Append(elem, reflect.ValueOf(1))
        
        dest = append(dest, 2)
        fmt.Println(dest)               // output [2]
        fmt.Println(elem.Interface())   // output [2]
    }

为什么两个对象的 len 会不相同呢,反射获取的 elem (代码第3行)如果使用 Set 赋值,它指向的地址也会被更新,也就是 dest 也会被更新。但如果使用 = 等于号赋值, elem 会覆盖为另一个地址,和 dest 指向的地址就脱节了。