標籤:

Go 語言的 slice 為啥有這樣的奇怪問題呢?

這樣一段程序

func main(){
s := []int{5}
s = append(s, 7)
s = append(s, 9)
x := append(s, 11)
y := append(s, 12)
fmt.Println(s, x, y)
}

輸出結果是

[5 7 9] [5 7 9 12] [5 7 9 12]

為什麼 x 的最後一位變成了 12 呢?

補充一下,如果改成這樣:

func main(){
s := []int{5, 7, 9}
x := append(s, 11)
y := append(s, 12)
fmt.Println(s,x,y)
}

結果就很正常

[5 7 9] [5 7 9 11] [5 7 9 12]


哈哈,如果沒有充分理解Slice的原理,這會是一個深坑。

實際過程如圖:

1. 創建s時,cap(s) == 1,內存中數據[5]

2. append(s, 7) 時,按Slice擴容機制,cap(s)翻倍 == 2,內存中數據[5,7]

3. append(s, 9) 時,按Slice擴容機制,cap(s)再翻倍 == 4,內存中數據[5,7,9],但是實際內存塊容量4

4. x := append(s, 11) 時,容量足夠不需要擴容,內存中數據[5,7,9,11]

5. y := append(s, 12) 時,容量足夠不需要擴容,內存中數據[5,7,9,12]

這就是後一次操作覆蓋了前一次操作數據的原因。

參考實驗:

package main

import "fmt"

func main(){
s := []int{5}

s = append(s,7)
fmt.Println("cap(s) =", cap(s), "ptr(s) =", s[0])

s = append(s,9)
fmt.Println("cap(s) =", cap(s), "ptr(s) =", s[0])

x := append(s, 11)
fmt.Println("cap(s) =", cap(s), "ptr(s) =", s[0], "ptr(x) =", x[0])

y := append(s, 12)
fmt.Println("cap(s) =", cap(s), "ptr(s) =", s[0], "ptr(y) =", y[0])
}

輸出:

cap(s) = 2 ptr(s) = 0x10328008
cap(s) = 4 ptr(s) = 0x103280f0
cap(s) = 4 ptr(s) = 0x103280f0 ptr(x) = 0x103280f0
cap(s) = 4 ptr(s) = 0x103280f0 ptr(y) = 0x103280f0

可以看出,s、x和y都指向同一個內存地址。


同學們,遇到問題要先 RTFM 啊! builtin - The Go Programming Language 這節說得很清楚:

The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself

換句話說 append 操作是有潛在副作用 (side effect) 的。從你這兩行

x := append(s, 11)
y := append(s, 12)

可以看出你沒有理解內建的 append 操作的本質。上面引用的那段手冊最後一句話也提到了這一點:如果你 append 過後的結果沒有 assign 回原來的 slice 變數,這種用法常常是錯的

你所想的情況很可能來自另外一些語言的經驗,比如在 Python 你可以這樣寫:

s = [5]
s = s + [7]
s = s + [9]
x = s + [11]
y = s + [12]
print s, x, y
# [5, 7, 9] [5, 7, 9, 11] [5, 7, 9, 12]

但需要注意的是這裡你做的並不是通常意義的 append 操作,而是所謂的 concatenate 操作。這兩者在語義上有很大的分歧:append 暗含 in-place update 的假設,而 concatenate 常常是 side-effect free。實際上 Python 列表的 append 方法 s.append(5) 也是 in-place 的。

從 UI 設計的角度看 Go 內建的 append 函數其實並不「直觀」。私以為更好的設計是做成 slice 對象的 append 方法會好一些,但這樣更「直觀」的設計完全不符合底層實現的客觀事實,會變成一個 leaky abstraction。現在這樣的結果算是一個 tradeoff 吧。每個語言都有自己的 nuances,看文檔的時候要留意。


如果語義上是需要兩個新slice,那就手工複製一份

package main

import "fmt"

func main() {
s := []int{5}
s = append(s, 7)
s = append(s, 9)

x := make([]int, len(s))
copy(x, s)
x = append(x, 11)

y := make([]int, len(s))
copy(y, s)
y = append(y, 12)

fmt.Println(s, x, y)
}

這樣就不用管len和cap了。

append操作不是一定要賦值回原變數。append操作是包含了ownership的變化的,x = append(s, 11)這句之後,如果編譯器能保證後面對s的引用都為非法,也就是s所指向的array的所有權已經剝奪了,不能再使用了,y = append(s, 12)這句不能通過編譯,那這個坑就不存在了。

但golang語言里是沒有所有權、move這些概念的,這樣的操作編譯器是不檢查的,於是就成了坑,需要人工檢查。如果能保證s不再使用(也就是不出現y = append(s, 12),s也不再使用),那可以直接append,否則就要copy。

這方面rust做得比較好。


@達達 已經回到的很好了。補充一下:

func main() {
s := []int{5, 7, 9}
x := append(s, 11)
y := append(s, 12)
fmt.Println(s, x, y)
}

之所以結果為 [5 7 9] [5 7 9 11] [5 7 9 12],是因為 s 的 cap(s) == 3,那麼:

x := append(s, 11)

y := append(s, 12)

重新分配數組的內存,x 和 y 指向了不同的數組。而前面的一段代碼,x 和 y 指向同一個數組,自然值就是一樣的了。


我補充下 @達達 的答案。首先,append的實現,golang官方有一個博文:Arrays, slices (and strings): The mechanics of append

裡面詳細介紹了append的實現原理:本質上append是在原有空間中添加,若空間不足時,採用 newSlice := make([]int, len(slice), 2*len(slice)+1)的方式進行擴容。

在空間不足的情況下,append在空間擴展之後,通過copy,將原有的slice拷貝到了新的newSlice中。因此,對擴容時,會有一個內存地址變化。但是如果在滿足空間大小時,內存地址不會發生變化,附加是用過內存操作實現的。slice從c語言上看是一個結構體,內部包含起始元素指針,長度等等信息。這個時候,x和y中元素的起始值指向的是相同的內存區域。當append時,s在go中僅為5.7.9,當第一次append時,操作的仍是原有的數據,s值為5,7,9,但是x中標記的值為5,7,9,11;第二次append時,12會被放置在11的位置,從而出現了影響x的值的情況。


Go 的 Slice 有個坑,當他預留空間夠用的時候就往後面添加,預留空間不夠的時候就申請新的內存把值複製過去。

實際上 s x y都是引用同一塊內存,只是len不一樣。

我也沒想明白為什麼會這樣。


這個得理解清楚slice的底層實現。slice源碼在src/runtime/slice.go

slice實際上是指向底層一個數組的指針,注意,要區分slice 和 數組,在go中這兩者差別很大,但是有些功能很相似,所以要先理清這兩者的區別。

看slice源碼中,slice結構體如下所示:

第一個元素是指向底層數組的指針,

第二個元素是這個slice的長度,則此slice表示的內容就是第一個元素開始,長度是此值的一個序列。

第三個元素是底層數組的容量。

因為slice長度是動態變化的,這樣,當slice長度發生變化時,其實其底層數組在內存中的地址也會發生變化,底層數據長度變化的規則是:

  • 如果新的大小是當前大小2倍以上,則大小增長為新大小
  • 否則循環以下操作:如果當前大小小於1024,按每次2倍增長,否則每次按當前大小1/4增長。直到增長的大小超過或等於新大小。

slice容量增長的源碼如下所示:

上面簡單介紹了slice的原理後,再回到題主的問題。

  • s := []int{5},s指向地址p1,len為1,底層數組容量為1
  • s = append(s, 7), 因為前面底層數組容量為1,執行了此條之後,底層數組需要擴容,容量變成2,s指向了新的底層數組地址p2。
  • s = append(s, 9) . 同樣,底層數組需要擴容,擴展成容量為4的底層數組,此時s指向了新的底層數組地址p3,len是3,cap是4
  • x := append(s, 11) . 此時,底層數組不需要擴容,因為容量是4,而現在只需要裝下四個元素,因此不需要擴容。注意,現在x指向了地址p3,其len是4,cap是4;而此時s還是和之前一樣,指向地址p3,len是3,cap是4。底層數組的元素是5 7 9 11
  • y := append(s, 12) . 注意,此行中的s還是和上一步一樣指向地址p3,len是3,cap是4,執行此行後,在s的基礎上將12append進來了,而s其實是指向地址p3,len是3,cap是4,因此將底層數組中第四個元素賦值成12(注意,上一步其實是將底層數組的第四個元素設置成了11,執行了現在這一條語句後就將第四個元素設置成12了,覆蓋掉了之前的11).因此底層數組現在是5,7,9,12。 【整個理解的關鍵在於這一步,關鍵在於對slice的底層數組的理解】
  • 執行完上面一步後,底層數組是5,7,9,12,底層數組的開始地址是p3;s指向地址p3,len是3,cap是4;x指向地址p3,len是4,cap是4;y指向地址p3,len是4,cap是4。因此x和y的元素其實是一樣的都是5,7,9,12

上面的過程就解釋為什麼最後x和y的結果一樣了。


首先,有幾點我們需要了解:

  1. 僅調用append()操作但不分配返回值是沒有任何意義的,例如:append(s, 10)並不會往s中添加任何值
  2. 把slice看成一個struct{addr, len, cap},創建一個新的slice的時候,實際上是創建了一個新的struct
  3. x := append(s, 10)實際上是創建了一個新的struct x,x的屬性如下:

len: s.len+1

cap: s.cap(len &<= s.cap),2*s.cap(len &> s.cap)

addr: 不變(cap未變,容量夠用,無需重新分配),改變(cap改變,需要擴容,重新分配一個底層數組,先將s的內容複製過去,再在複製後的數組中執行append,最後將x的addr指向該數組)

題主所示的代碼執行的過程如下(*表示本次操作後內存地址較上次已經改變):

第1段
|====================================================
|var |ele |len() |cap() |mem
|s |[5] |1 |1 |0x1234
|s(append(s,7)) |[5,7] |2 |2 |0xaaaa(*)
|s(append(s,9)) |[5,7,9] |3 |4 |0x8888(*)
|x(append(s,11) |[5,7,9,11] |4 |4 |0x8888
|y(append(s,12)) |[5,7,9,12] |4 |4 |0x8888
|====================================================

第2段
|====================================================
|var |ele |len() |cap() |mem
|s |[5,7,9] |3 |3 |0x1234
|x(append(s,11) |[5,7,9,11] |4 |6 |0xaaaa(*)
|y(append(s,12)) |[5,7,9,12] |4 |6 |0x8888(*)
|====================================================

我們來分析第1段

  1. x := append(s, 11)的時候,由於cap較上次未變,所以直接在底層數組添加了11(在s.len處即s的末尾),此時x中是[5,7,9,11]
  2. y := append(s, 12)的時候,由於cap較上次未變(這裡注意,上次append結果是分配給了x,s並沒有任何改變),所以仍然在底層數組添加了12(依然是在s.len處即s的末尾添加),所以此時底層數組最後一個元素(s.len處)被改成了12(如果在本步驟之前列印是可以看到最後一個元素是11的)
  3. 最後列印輸出的時候,由於x,y引用的是同一個數組,所以結果一樣

同樣,來分析第2段

  1. x := append(s, 11)的時候,由於s的容量不夠需要擴容,cap變了,所以重新創建了底層數組(cap為2*s.cap,即6),並將s的內容都複製了過來,然後添加了11,此時x中是[5,7,9,11]
  2. y := append(s, 12)的時候,由於s的容量仍然不夠需要擴容,又創建了一個底層數組(cap為2*s.cap,即6),並將s的內容都複製過來,然後再添加了12,此時y中是[5,7,9,12]
  3. 此時的x和y沒有任何關係,所以列印輸出的時候是各自的值,也就是我們正常所期待的結果


理解不了


反正寫代碼新切片都直接用copy生成


s的len為3,x,y的len為4。s,x,y的實際數組又是指向同一塊內存地址,so....


package main

import "fmt"

func sliceTest(sli []int) {
fmt.Printf("Address of sli is: %d
", sli[0])
}

func main() {

// 聲明數組
v_IntArray := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("v_IntArray is: ", v_IntArray)

// 對數組進行切片,方括弧裡面數字的意義為 `[low, high, capacity]`, 對應數組索引 `[starterIndex, endIndex + 1, lengthOfArray - starterIndex]`
v_IntSlice := v_IntArray[1:4:9]
fmt.Println("v_IntArray[1:4:9], v_IntSlice is: ", v_IntSlice)
fmt.Println("v_IntArray[1:4:9], len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("v_IntArray[1:4:9], cap(v_IntSlice) is: ", cap(v_IntSlice))

// 省略 `high` 和 `capacity`,省略 `high` 會從數組索引為 `low` 的元素一直到數組最後一個元素,切片的容量為 `lengthOfArray - starterIndex`
v_IntSlice = v_IntArray[1:]
v_IntSlice[0] = 100
fmt.Println("v_IntArray is: ", v_IntArray)
fmt.Println("v_IntArray[1:], v_IntSlice is: ", v_IntSlice)
fmt.Println("v_IntArray[1:], len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("v_IntArray[1:], cap(v_IntSlice) is: ", cap(v_IntSlice))

// 省略 `low` 和 `capacity`,省略 `low` 會從數組索引為 `0` 的元素一直到索引為 `high` 的元素,切片的容量和數組長度相等
v_IntSlice = v_IntArray[:1]
fmt.Println("v_IntArray[:1], v_IntSlice is: ", v_IntSlice)
fmt.Println("v_IntArray[:1], len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("v_IntArray[:1], cap(v_IntSlice) is: ", cap(v_IntSlice))

// 全部省略,從數組索引為 `0` 的元素一直到數組最後一個元素,切片的容量和數組長度相等
v_IntSlice = v_IntArray[:]
fmt.Println("v_IntArray[:], v_IntSlice is: ", v_IntSlice)
fmt.Println("v_IntArray[:], len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("v_IntArray[:], cap(v_IntSlice) is: ", cap(v_IntSlice))

// 省略 `low`,省略 `low` 會從數組索引為 `0` 的元素一直到索引為 `high` 的元素,切片對象的 `capacity` 為給定的數值,注意這個數值不能大於數組的長度
v_IntSlice = v_IntArray[:4:4]
fmt.Println("v_IntArray[:4:4], v_IntSlice is: ", v_IntSlice)
fmt.Println("v_IntArray[:4:4], len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("v_IntArray[:4:4], cap(v_IntSlice) is: ", cap(v_IntSlice))

// 如果所有初始值已經確定,可以這樣直接聲明 `slice` 對象
v_IntSlice = []int{1, 2, 5: 6}
v_IntSlice[0] = 100
fmt.Println("v_IntArray is: ", v_IntArray)
fmt.Println("[]int{1, 2, 5: 6}, v_IntSlice is: ", v_IntSlice)

// 如果需要之後進行 `slice` 對象賦值,可以使用 `make` 內置函數聲明 `slice` 對象
v_IntSlice = make([]int, 5, 10)
fmt.Println("make([]int, 5, 10), v_IntSlice is: ", v_IntSlice)
fmt.Println("make([]int, 5, 10), len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("make([]int, 5, 10), cap(v_IntSlice) is: ", cap(v_IntSlice))

// 使用 `make` 內置函數聲明 `slice` 對象的時候可以省略 `capacity`,其默認值和給定的 `slice` 的 `length` 相等。
v_IntSlice = make([]int, 5)
fmt.Println("make([]int, 5), v_IntSlice is: ", v_IntSlice)
fmt.Println("make([]int, len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("make([]int, cap(v_IntSlice) is: ", cap(v_IntSlice))

// 使用 `new` 內置函數也可以聲明 `slice` 對象
v_IntSlice = new([10]int)[:5]
fmt.Println("new([10]int)[:5], v_IntSlice is: ", v_IntSlice)
fmt.Println("new([10]int)[:5], len(v_IntSlice) is: ", len(v_IntSlice))
fmt.Println("new([10]int)[:5], cap(v_IntSlice) is: ", cap(v_IntSlice))

// 可以對切片進行再切片
v_IntSlice = make([]int, 5)
v_AnotherIntSlice := v_IntSlice[3:]
v_AnotherIntSlice[0] = 100
fmt.Println("v_AnotherIntSlice = v_IntSlice[3:], len(v_AnotherIntSlice) is: ", len(v_AnotherIntSlice))
fmt.Println("v_AnotherIntSlice = v_IntSlice[3:], cap(v_AnotherIntSlice) is: ", cap(v_AnotherIntSlice))
fmt.Println("v_AnotherIntSlice = v_IntSlice[3:], v_IntSlice is: ", v_IntSlice)
fmt.Println("v_AnotherIntSlice = v_IntSlice[3:], v_AnotherIntSlice is: ", v_AnotherIntSlice)

// 使用 `append` 內置函數可以對 `slice` 對象在其容量範圍內進行添加,新添加的元素索引為 `len(切片對象)`,操作返回新的切片對象
v_IntSlice = make([]int, 1, 2)
v_AnotherIntSlice = append(v_IntSlice, 100)
fmt.Println("v_AnotherIntSlice = append(v_IntSlice, 100), v_IntSlice is: ", v_IntSlice)
fmt.Println("v_AnotherIntSlice = append(v_IntSlice, 100), v_AnotherIntSlice is: ", v_AnotherIntSlice)
fmt.Printf("Address of v_IntSlice is: %d
", v_IntSlice[0])
fmt.Printf("Address of v_AnotherIntSlice is: %d
", v_AnotherIntSlice[0])

// 如果使用 `append` 追加元素的時候超出了切片對象的容量,Golang 會重新創建一個匿名數組來保存新的切片對象中的數據
v_AnotherIntSlice = append(v_IntSlice, 100, 200)
fmt.Println("v_AnotherIntSlice = append(v_IntSlice, 100, 200), v_IntSlice is: ", v_IntSlice)
fmt.Println("v_AnotherIntSlice = append(v_IntSlice, 100, 200), v_AnotherIntSlice is: ", v_AnotherIntSlice)
fmt.Printf("Address of v_IntSlice is: %d
", v_IntSlice[0])
fmt.Printf("Address of v_AnotherIntSlice is: %d
", v_AnotherIntSlice[0])

// 使用切片對象作為函數參數傳遞時是值傳遞,但是由於 `slice` 是引用類型,所以不會拷貝相關數組的值
v_IntSlice = make([]int, 5, 10)
fmt.Printf("Address of v_IntSlice is: %d
", v_IntSlice[0])
sliceTest(v_IntSlice)

}


推薦閱讀:

如何理解 slice 的 capacity?
如果編碼規範要求 Go 代碼每行不超過 100 個字元,是否合理,為什麼?
golang寫法疑問?
系統學習GO,推薦幾本靠譜的書?
王垠噴 Go 語言,許式偉贊 Go 語言,大家怎麼看?

TAG:Go語言 |