介绍
在 Go 语言中,切片类型比较常用,将新元素追加到切片也比较常见,因此 Go 语言提供一个内置函数 append
,该函数可以非常方便实现此功能。
虽然 Go 语言内置函数 append
使用非常方便,但是使用不当会不小心掉入一些“坑”。
本文我们介绍一下 Go 语言为什么建议 append
追加新元素使用原切片变量接收返回值?
append 的“坑”
我们先看一段示例代码:
func main() { a := make([]int, 0, 5) a = append(a, 1) b := append(a, 2) c := append(a, 3) fmt.Printf("v=%v || p=%p\n", a, &a) // v=[1] || p=0xc00000c060 fmt.Printf("v=%v || p=%p\n", b, &b) // v=[1 3] || p=0xc00000c080 fmt.Printf("v=%v || p=%p\n", c, &c) // v=[1 3] || p=0xc00000c0a0 }
阅读上面这段代码,我们定义一个长度为 0,容量为 5 的 int 类型的切片 a。
首先,我们使用 Go 语言内置函数 append
追加一个元素 1
到切片 a 中。
然后,我们使用 Go 语言内置函数 append
追加一个元素 2
到切片 a 中。
最后,我们使用 Go 语言内置函数 append
追加一个元素 3
到切片 a 中。
但是,我们在输出结果中发现,b 的输出结果不是 [1 2]
,c 的输出结果不是 [1 2 3]
,b 和 c 的实际输出结果相同,都是 [1 3]
。为什么呢?我们接着往下看 Part 03 的内容。
03
append 的原理
Go 语言内置函数 append
第一个入参是切片类型的变量,而切片本身是一个 struct 结构,参数传递时会发生值拷贝。
Go 语言 slice 源码如下:
type slice struct { array unsafe.Pointer len int cap int }
因为 Go 语言内置函数 append
参数是值传递,所以 append
函数在追加新元素到切片时,append
会生成一个新切片,并且将原切片的值拷贝到新切片。
在 Part 02 示例代码中,我们三次使用 append
参数追加新元素到切片 a 的操作,接收返回值的变量都不同。
第二次操作时,因为 append
生成一个新切片,将原切片 a 的值拷贝到新切片,并且将新元素在原切片a[len(a)]
长度的位置开始追加,使用变量 b 接收 append
返回值 [1 2]
,所以变量 b 的值是 [1 2]
。
第三次操作时,同样 append
生成一个新切片,将原切片 a 的值拷贝到新切片,并且将新元素在原切片a[len(a)]
长度的位置开始追加,使用变量 c 接收 append
返回值 [1 3]
,所以变量 c 的值是 [1 3]
。
但是,因为三个切片的底层数组相同,Go 内置函数 append
会在原切片长度的位置开始追加新元素,所以第三次操作时,把第二次操作时得到的变量 b 的最后一个元素覆盖了。
阅读到这里,相信聪明的读者朋友们已经明白 Part 02 示例代码为什么实际输出结果和预想的输出结果不同了吧。
04
总结
本文我们介绍 Go 语言中使用内置函数 append
追加新元素的一个“坑”,建议读者朋友们使用原切片变量接收返回值。
推荐阅读:
参考资料:
- https://go.dev/tour/moretypes/15
- https://pkg.go.dev/builtin#append
- https://go.dev/blog/slices-intro
- https://go.dev/doc/effective_go#slices
- https://go.dev/ref/spec#Slice_types
- https://go.dev/ref/spec#Length_and_capacity
- https://go.dev/ref/spec#Making_slices_maps_and_channels
- https://go.dev/ref/spec#Appending_and_copying_slices