Go 中使用 append 多次操作并赋值 slice,为什么原始值可能被改变?

在使用 append 操作 slice 赋值给新 slice 时,可能会遇到中间赋值的 slice 值被改变的非预期情况,比如说如下的代码:

func main(){
    s := []int{5}
    s = append(s, 7)
    s = append(s, 9)
    x := append(s, 11)
    y := append(s, 12)
   fmt.Println(s, x, y)
}

实际输出的结果是 [5 7 9] [5 7 9 12] [5 7 9 12],并非预期的 [5 7 9] [5 7 9 11] [5 7 9 12]

比较敏感的同学应该一下会猜到跟内存地址有关系,实际上这种情况也是对 slice 和 append 的特性不够了解造成的。

首先我们要知道 slice 数据结构包含两个内部变量 len(已有数据长度)和 cap(总共可容纳数据量)。当使用 append 操作 slice 时,如果 cap > len,即容量够用,便会继续使用当前的空间,那么内存地址不会变化。如果 cap == len,空间不够用了,就会对 slice 进行自动扩容,cap 变为两倍,此时会申请新空间,也就是返回了新的 slice,内存地址也就发生了变化。

在上述例子中,初始的 slice 大小为 1, 在第二、三步时都需要扩容。而第四、五步都是以 s 为基础,容量够用,因此没有扩容申请新空间,因此便将同一个地址赋给了 x 和 y。

动作 地址 数据 cap
s := []int{5} 0x1111 5 1
s = append(s, 7) 0x2222 5,7 2
s = append(s, 9) 0x3333 5,9 4
x := append(s, 11) 0x2222 5,7,9,11 2
y := append(s, 12) 0x2222 5,7,9,12 2

因为四五步操作的是同一个地址上的数据,因此 x 的值自然就被 y 的值覆盖了。

而如果是用下面的代码,s 的大小初始为 3,那么每次操作都需要扩容,都会返回新空间,那么结果就符合我们的预期了。

func main() {
	s := []int{5, 7, 9}
	x := append(s, 11)
	y := append(s, 12)
	fmt.Println(s, x, y)
}

如何避免或解决这一问题呢

其实解决办法也很简单,就是当要赋值给新的 slice 的时候,都重新申请空间并使用 copy 复制就可以了。类似如下代码:

func main(){
	s := []int{5}
	s = append(s, 7)
	s = append(s, 9)
	x := make([]int, len(s))
	copy(x, s)
	x = append(x, 11)
	y := append(s, 12)
	fmt.Println(s, x, y)
}

最后可能有人会有疑问:既然 x 和 y 都是使用的和 s 相同的内存,那为什么打印出的 s 和 x、y 结果不一样呢?

实际上虽然指向同一块内存,但是他们的 len 是不同的,所以打印出来的结果是不一样的


735 Words

2020-04-24 08:00 +0800