Go 語言為什么建議 append 追加新元素使用原切片變量接收返回值?
?1.介紹
在 Go 語言中,切片類型比較常用,將新元素追加到切片也比較常見,因此 Go 語言提供一個內置函數 append,該函數可以非常方便實現此功能。
雖然 Go 語言內置函數 append 使用非常方便,但是使用不當會不小心掉入一些“坑”。
本文我們介紹一下 Go 語言為什么建議 append 追加新元素使用原切片變量接收返回值?
2.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)
fmt.Printf("v=%v || p=%p\n", b, &b)
fmt.Printf("v=%v || p=%p\n", c, &c)
}
閱讀上面這段代碼,我們定義一個長度為 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 的內容。
3.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 示例代碼為什么實際輸出結果和預想的輸出結果不同了吧。
4.總結
本文我們介紹 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