這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
title: "Go切片內容丟失問題"
date: 2017-10-24
draft: false
categories: ["Golang"]
tags: [""]
感覺這篇文章有點標題黨的嫌疑, 文中要說的其實還是Go的值傳遞的問題, 不是發現了Go的某些驚天大bug,誤入的大神可以不用看下面的內容啦 !
起因是在編寫公司一個項目的過程中, 發現如果在結構體的不同方法之間調用其包含的切片,會出現切片內容不一致的情況.代碼類似這樣:
type T struct { slice []int}func (t T) Append() { t.slice = append(t.slice, 1) fmt.Println("#1 slice:",t.slice)}func (t T) Show() { fmt.Println("#2 slice:", t.slice)}func main() { t := T{} t.slice = make([]int, 0, 1) t.Append() t.Show()}
執行上面的代碼會輸出:
#1 slice: [1]#2 slice: []
按照正常的思維, 切片應該都是輸出[1]才對,但其實因為Go的值傳遞模型, 導致了結果與預料的出現了偏差.之前以為自己對這個問題徹底搞懂了,沒想到還是會糾結了一下, 下面記錄下解決問題的過程,希望自己徹底理解Go的值傳遞模型,也希望能對你有協助.
為上面的代碼添加一些列印輸出:
type T struct { slice []int}func (t T) Append() { fmt.Printf("Append: %p\n", &t) t.slice = append(t.slice, 1)}func (t T) Show() { fmt.Printf("Show: %p\n", &t)}func main() { t := T{} fmt.Printf("main: %p\n", &t) t.slice = make([]int, 0, 1) t.Append() t.Show()}
輸出為
main: 0x1040a0b0Append: 0x1040a0c0Show: 0x1040a0d0
可以看到, 三個變數t的地址都不相同, 所以在調用某個類型的方法的時候, Go是會拷貝一個新的值,確實是使用值傳遞.
接下來列印slice的地址出來看看(我在這裡犯了一個錯誤)
type T struct { slice []int}func (t T) Append() { t.slice = append(t.slice, 1) fmt.Printf("Append slice: %p\n", t.slice)}func (t T) Show() { fmt.Printf("Show slice: %p\n", t.slice)}func main() { t := T{} t.slice = make([]int, 0, 1) fmt.Printf("main slice: %p\n", t.slice) t.Append() t.Show()}
輸出為:
main slice: 0x10410020Append slice: 0x10410020Show slice: 0x10410020
看到這裡輸出的地址相同, 我誤以為調用類型方法的時候,對切片沒有使用值傳遞.但其實不然
Go的切片本質是也是一個結構體, 在源碼中可以看到它的定義:
// src/runtime/slice.gotype slice struct { array unsafe.Pointer len int cap int}
我們上面輸出的3個地址都是這個結構體的地址.但不是說Go是值傳遞的嗎,為什麼這裡又傳了引用?
其實Go在這裡還是使用了值傳遞, 但對於slice, map, channel這三種應用類型, Go又不會做全部的複製處理, 如果這樣做顯然開銷太大.
Go默默得為我們建立了一個變數, 然後將這個變數的值指向了切片.說起來有點繞, 用一張圖來表示會更直白:
image.png
我們列印看中 t ' 對應的slice的地址看看:
type T struct { slice []int}func (t T) Append() { t.slice = append(t.slice, 1) fmt.Printf("Append slice: %p\n", &t.slice)}func (t T) Show() { fmt.Printf("Show slice: %p\n", &t.slice)}func main() { t := T{} t.slice = make([]int, 0, 1) fmt.Printf("main slice: %p\n", &t.slice) t.Append() t.Show()}
輸出的3個地址確實都不同:
main slice: 0x1040a0b0Append slice: 0x1040a0c0Show slice: 0x1040a0d0
總結:
Go裡面的參數傳遞其實都是按照值傳遞來進行的, 理解這點可以解決很多開發中遇到的問題.至於如何解決上面代碼中切片元素不一致的問題 ? 很簡單, 把類型方法中的T改為指標類型就可以了.