切片append操作的易错点

less than 1 minute read

看下面这段程序:

func main() {
	a := []int{1, 2, 3}
	fmt.Println(len(a), cap(a))

	b := append(a, 100)
	fmt.Println(len(b), cap(b))

	c := append(a, 101)
	fmt.Println(len(c), cap(c))
	fmt.Println(a, b, c)
}

打印出的a,b,c是[1 2 3] [1 2 3 100] [1 2 3 101],符合预期。 但是如果把a的赋值改成如下形式,结果又是什么呢?

var a []int
for i := 1; i <= 3; i++ {
    a = append(a, i)
}

此时,打印出的a,b,c变成了[1 2 3] [1 2 3 101] [1 2 3 101],b和c居然是一样的,出现了bug!这是为何?

注意到两段程序中a,b,c切片的长度和容量分别是: (1)第一段程序:len(a)=3, cap(a)=3, len(b)=4, cap(b)=6, len(c)=4, cap(c)=6 (2)第二段程序:len(a)=3, cap(a)=4, len(b)=4, cap(b)=4, len(c)=4, cap(c)=4

分析: (1)在第一段程序中,切片a是用字面量声明的,其长度和容量相等。当执行append操作时,都需要开辟内存空间,对切片a复制后,再添加元素。所以切片b和c是不相干的。同时注意到,b和c的容量是a的容量的两倍,这正是切片的扩容方法。 (2)在第二段程序中,切片a是通过逐个添加元素得到的,容量比长度大,有继续添加元素的空间。当执行append操作时,无需再开辟内存空间,直接对切片a的底层数组做修改即可。所以切片b和c是共用同一个底层数组的(更全面地说,a,b,c都共用同一个底层数组)。执行b := append(a, 100)后,底层数组元素为[1 2 3 100];执行c := append(a, 101)后,底层数组元素为[1 2 3 101],这样一来,b和c也就一样了。

正确的做法:避免b和c共用底层数组。

func main() {
	var a []int
	for i := 1; i <= 3; i++ {
		a = append(a, i)
	}
	fmt.Println(len(a), cap(a))

	b := append(append([]int{}, a...), 100)
	fmt.Println(len(b), cap(b))

	c := append(append([]int{}, a...), 101)
	fmt.Println(len(c), cap(c))
	fmt.Println(a, b, c)
}

补充阅读:Golang slices gotcha 一文中有图形解释,很形象了。

Tags: ,

Categories:

Updated:

Comments