在用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开始的每一个元素,这里是4
和5
。
最后,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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。