[golang]slice的坑:從append到共享

一桶冷水發表於2018-09-29

go是宣揚實用主義的語言,很多時候都把c中的最佳實踐直接規定成語法了。其中之一就是slice,簡單但是非常容易踩坑。

先看一個小例子:

func main() {
	a := make([]int, 2, 2)
	a[0], a[1] = 1, 2

	b := append(a[0:1], 3)
	c := append(a[1:2], 4)

	fmt.Println(b,c)
}
複製程式碼

在這個小例子中,原本是希望將a[0:1]作為b的字首,然後追加上3;將a[1:2]作為c的字首,然後追加上4。但實際上輸出結果並不是原本期望的[1 3] [2 4],而變成了[1 3] [3 4]。這是為什麼呢?

我們知道資料結構中陣列是非常高效的,可以直接定址,但是有個缺陷,難以擴容。所以slice被設計為指向陣列的指標,在需要擴容時,會將底層陣列上的值複製到一個更大的陣列上然後指向這個新陣列。

slice有個特性是允許多個slice指向同一個底層陣列,這是一個有用的特性,在很多場景下都能通過這個特性實現 no copy 而提高效率。但共享同時意味著不安全。b在追加3時實際上覆蓋了a[1],導致c變成了[3 4]

怎麼解決呢?防止共享資料的出現問題需要注意兩條,只讀和複製,或者統一歸納為不可變。

寫法1,make出一個新slice,然後先copy字首到新陣列上再追加:

func main() {
	a := make([]int, 2, 2)
	a[0], a[1] = 1, 2

	b := make([]int, 1)
	copy(b, a[0:1])
	b = append(b, 3)

	c := make([]int, 1)
	copy(c, a[1:2])
	c = append(c, 4)

	fmt.Println(b, c)
}
複製程式碼

寫法2,利用go中slice的一個小眾語法,a[0:1:1] (源[起始index,終止index,cap終止index]),強迫追加時複製到新陣列。

func main() {
	a := make([]int, 2, 2)
	a[0], a[1] = 1, 2

	b := append(a[0:1:1], 3)

	c := append(a[1:2:2], 4)

	fmt.Println(b, c)
}
複製程式碼

相關文章