切片傳遞的隱藏危機

提出疑問golang


在Go的源碼庫或者其餘開源項目中,會發現有些函數在須要用到切片入參時,它採用是指向切片類型的指針,而非切片類型。這裏未免會產生疑問:切片底層不就是指針指向底層數組數據嗎,爲什麼不直接傳遞切片,二者有什麼區別web

例如,在源碼log包中,Logger對象上綁定了formatHeader方法,它的入參對象buf,其類型是*[]byte,而非[]byte數組

1func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {}

有如下例子微信

 1func modifySlice(innerSlice []string) {
2    innerSlice[0] = "b"
3    innerSlice[1] = "b"
4    fmt.Println(innerSlice)
5}
6
7func main() {
8    outerSlice := []string{"a""a"}
9    modifySlice(outerSlice)
10    fmt.Print(outerSlice)
11}
12
13// 輸出以下
14[b b]
15[b b]

咱們將modifySlice函數的入參類型改成指向切片的指針app

 1func modifySlice(innerSlice *[]string) {
2    (*innerSlice)[0] = "b"
3    (*innerSlice)[1] = "b"
4    fmt.Println(*innerSlice)
5}
6
7func main() {
8    outerSlice := []string{"a""a"}
9    modifySlice(&outerSlice)
10    fmt.Print(outerSlice)
11}
12
13// 輸出以下
14[b b]
15[b b]

在上面的例子中,兩種函數傳參類型獲得的結果都同樣,彷佛沒發現有什麼區別。經過指針傳遞它看起來毫無用處,並且不管如何切片都是經過引用傳遞的,在兩種狀況下切片內容都獲得了修改。函數

這印證了咱們一向的認知:函數內對切片的修改,將會影響到函數外的切片。但,真的是如此嗎?工具


考證與解釋學習


在《你真的懂string與[]byte的轉換了嗎》一文中,咱們講過切片的底層結構以下所示。測試

1type slice struct {
2    array unsafe.Pointer
3    len   int
4    cap   int
5}

array是底層數組的指針,len表示長度,cap表示容量。ui

咱們對上文中的例子,作如下細微的改動。

 1func modifySlice(innerSlice []string) {
2    innerSlice = append(innerSlice, "a")
3    innerSlice[0] = "b"
4    innerSlice[1] = "b"
5    fmt.Println(innerSlice)
6}
7
8func main() {
9    outerSlice := []string{"a""a"}
10    modifySlice(outerSlice)
11    fmt.Print(outerSlice)
12}
13
14// 輸出以下
15[b b a]
16[a a]

神奇的事情發生了,函數內對切片的修改居然沒能對外部切片形成影響?

爲了清晰地明白髮生了什麼,將打印添加更多細節。

 1func modifySlice(innerSlice []string) {
2    fmt.Printf("%p %v   %p\n", &innerSlice, innerSlice, &innerSlice[0])
3    innerSlice = append(innerSlice, "a")
4    innerSlice[0] = "b"
5    innerSlice[1] = "b"
6    fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
7}
8
9func main() {
10    outerSlice := []string{"a""a"}
11    fmt.Printf("%p %v   %p\n", &outerSlice, outerSlice, &outerSlice[0])
12    modifySlice(outerSlice)
13    fmt.Printf("%p %v   %p\n", &outerSlice, outerSlice, &outerSlice[0])
14}
15
16// 輸出以下
170xc00000c060 [a a]   0xc00000c080
180xc00000c0c0 [a a]   0xc00000c080
190xc00000c0c0 [b b a] 0xc000022080
200xc00000c060 [a a]   0xc00000c080

在Go函數中,函數的參數傳遞均是值傳遞。那麼,將切片經過參數傳遞給函數,其實質是複製了slice結構體對象,兩個slice結構體的字段值均相等。正常狀況下,因爲函數內slice結構體的array和函數外slice結構體的array指向的是同一底層數組,因此當對底層數組中的數據作修改時,二者均會受到影響。

可是存在這樣的問題:若是指向底層數組的指針被覆蓋或者修改(copy、重分配、append觸發擴容),此時函數內部對數據的修改將再也不影響到外部的切片,表明長度的len和容量cap也均不會被修改。

爲了讓讀者更清晰的認識到這一點,將上述過程可視化以下。

能夠看到,當切片的長度和容量相等時,發生append,就會觸發切片的擴容。擴容時,會新建一個底層數組,將原有數組中的數據拷貝至新數組,追加的數據也會被置於新數組中。切片的array指針指向新底層數組。因此,函數內切片與函數外切片的關聯已經完全斬斷,它的改變對函數外切片已經沒有任何影響了。

注意,切片擴容並不老是等倍擴容。爲了不讀者產生誤解,這裏對切片擴容原則簡單說明一下(源碼位於src/runtime/slice.go 中的 growslice 函數):

切片擴容時,當須要的容量超過原切片容量的兩倍時,會直接使用須要的容量做爲新容量。不然,當原切片長度小於1024時,新切片的容量會直接翻倍。而當原切片的容量大於等於1024時,會反覆地增長25%,直到新容量超過所須要的容量。

到此,咱們終於知道爲何有些函數在用到切片入參時,它須要採用指向切片類型的指針,而非切片類型。

 1func modifySlice(innerSlice *[]string) {
2    *innerSlice = append(*innerSlice, "a")
3    (*innerSlice)[0] = "b"
4    (*innerSlice)[1] = "b"
5    fmt.Println(*innerSlice)
6}
7
8func main() {
9    outerSlice := []string{"a""a"}
10    modifySlice(&outerSlice)
11    fmt.Print(outerSlice)
12}
13
14// 輸出以下
15[b b a]
16[b b a]

請記住,若是你只想修改切片中元素的值,而不會更改切片的容量與指向,則能夠按值傳遞切片,不然你應該考慮按指針傳遞。


例題鞏固


爲了判斷讀者是否已經真正理解上述問題,我將上面的例子作了兩個變體,讀者朋友們能夠自測。

測試一

 1func modifySlice(innerSlice []string) {
2    innerSlice[0] = "b"
3  innerSlice = append(innerSlice, "a")
4    innerSlice[1] = "b"
5    fmt.Println(innerSlice)
6}
7
8func main() {
9    outerSlice := []string{"a""a"}
10    modifySlice(outerSlice)
11    fmt.Println(outerSlice)
12}

測試二

 1func modifySlice(innerSlice []string) {
2    innerSlice = append(innerSlice, "a")
3    innerSlice[0] = "b"
4    innerSlice[1] = "b"
5    fmt.Println(innerSlice)
6}
7
8func main() {
9    outerSlice:= make([]string03)
10    outerSlice = append(outerSlice, "a""a")
11    modifySlice(outerSlice)
12    fmt.Println(outerSlice)
13}

測試一答案

1[b b a]
2[b a]

測試二答案

1[b b a]
2[b b]

你作對了嗎?









往期推薦



Golang技術分享


長按識別二維碼關注咱們

更多golang學習資料

回覆關鍵詞1024



本文分享自微信公衆號 - Golang技術分享(gh_1ac13c0742b7)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索