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)
}
複製程式碼