在用Go时发现的一个小坑

在Python中,返回一个去除原列表中下标为i的元素的新列表,可以用切片语法,将下标i前后的列表切开再组合成一个新的切片,这是显而易见的。

list1=[1,2,3,4,5]
list2=list1[:2]+list1[3:]
print(list1,list2)
# [1, 2, 3, 4, 5] [1, 2, 4, 5]

但在Go中,似乎没有这么想当然。如果想当然的用append()去切割合并切片,会影响原来的切片。

slice1 := []int{1, 2, 3, 4, 5}
fmt.Println(slice1)
slice2 := append(slice1[:2], slice1[3:]...)
fmt.Println(slice2)
fmt.Println(slice1)
// 输出
// [1 2 3 4 5]
// [1 2 4 5]
// [1 2 4 5 5]

为什么会这样?首先,Go的切片只是个结构体,包含了一个指向底层数组的指针、长度、容量。当我们使用切片语法b:=a[low:high]时,新的切片b只是获取了一个结构体,包含指向a底层数组的指针,并且可能有不同的长度和容量。

关键在于这个指向a底层数组的指针,这意味着,go中的切片,只是对某个数组的引用。对某个切片或数组反复进行切割,产生的切片只是对同一个底层数组的引用,他们之间会互相影响。

其次我们要了解append()函数做了什么

func append(slice []Type, elems ...Type) []Type

append会接受一个切片slice,然后是一系列元素。如果slice添加元素后超过自身容量,就会发生扩容(扩容的具体规则这里并不讨论),append会申请一个新的更大的数组。最后返回一个新切片,新切片指向一个新的底层数组。这个是大部分情况下用append()时的场景。

但是如果slice添加元素后没有超过容量会怎么样?那返回的新切片就会继续使用原来的底层数组。在某些情况下,比如当你通过原切片返回一个去除某个元素的新切片,并且不影响原切片。当你使用了这样的语法,就会出错

slice2 := append(slice1[:2], slice1[3:]...)

这里发生了什么?用图表表示的话

原本slice1是这样的

经过slice2 := append(slice1[:2], slice1[3:]...)

append()收到slice1[:2],这是一个和slice1共用底层数组的切片。指针指向slice1的第一个元素,长度为2,容量为5。(容量的意思就是,一个切片的第1个元素,到底层数组的最后1个元素,一共有多少个元素。)
append()收到的第二个参数是slice1[3:]...,这意味着,逐个添加slice1从下标3开始的每一个元素,这里是45
最后,slice2添加了2个元素,从长度2增长到了长度4。也就是没有超过容量,因此没有发生扩容。所以slice2仍然使用slice1的底层数组,造成了对slice1的影响。

那该怎么办呢?

普遍的解决方案是,构造新切片,然后复制原切片的部分数据到新切片。
具体如何构造,可以先创建空切片,然后用append()添加元素,空切片容量为0,这会导致append创建新的底层数组,不会影响原切片。我们可以输出这两个切片的首元素地址看看。

slice1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice1,%v%T%p\n", slice1, slice1, slice1)
slice2 := append([]int(nil),slice1[:2]...)
slice2 = append(slice2,slice1[3:]...)
fmt.Printf("slice2,%v%T%p\n", slice2, slice2, slice2)
fmt.Printf("slice1,%v%T%p\n", slice1, slice1, slice1)
// 输出
// slice1,[1 2 3 4 5][]int0xc000018330
// slice2,[1 2 4 5][]int0xc00001a2a0
// slice1,[1 2 3 4 5][]int0xc000018330

rwxe
91 声望5 粉丝

no tengo trabajo.