目錄:[ - ]
OSC源創會主題補充系列:
Go語言規范雖然很簡單, 但是深入掌握Go語言卻需要很多底層知識.
本來第20期的武漢OSC源創會有Go語言的專題講座, 誰知道說取消就取消了.
我最近也整理了一些Go語言資料, 有Go語言的歷史/現狀/未來發展的八卦和Go語言常見的問題和陷阱兩個部分, 本來打算OSC源創會能和武漢的Gopher分享 下的, 誰知道(由于不是贊助商也不是微軟的大牛)主辦方根本不給任何的機會.
100+人數的交流會基本都是扯淡, 還是小規模的討論沙龍比較靠譜, 以后再也不會去OSC源創會當聽眾了.
現在計劃將各個小問題暫時作為博客發表.
傳參和傳引用的問題
很多非官方的文檔和教材(包括一些已經出版的圖書), 對Go語言的傳參和引用的講解 都有很多問題. 導致眾多Go語言新手對Go的函數參數傳參有很多誤解.
而傳參和傳引用是編程語言的根本問題, 如果這個問題理解錯誤可能會導致很多問題.
slice不是引用!
首先, Go語言的函數調用參數全部是傳值的, 包括 slice
/map
/chan
在內所有類型, 沒有傳引用的說法.
具體請看Go語言的規范:
After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.
什么叫引用?
比如有以下代碼:
var a Object doSomething(a) // 修改a的值 print(a)
如果函數doSomething
修改a
的值, 然后print
打印出來的也是修改后的值, 那么就可以認為doSomething
是通過引用的方式使用了參數a
.
為什么slice不是引用?
我們構造以下的代碼:
func main() { a := []int{1,2,3} fmt.Println(a) modifySlice(a) fmt.Println(a) } func modifySlice(data []int) { data = nil }
其中modifySlice
修改了切片a
, 輸出結果如下:
[1 2 3] [1 2 3]
說明a
在調用modifySlice
前后并沒有任何變化, 因此a
必然是傳值的!
為什么很多人誤以為slice是引用呢?
可能是 因為很多新接觸Go語言的新手, 看到Go語言的文檔說Go的切片和C語言的數組類型, 而C語言的數組是傳地址的(注意: 不是傳引用!).
下面這個代碼可能是錯誤的根源:
func main() { a := []int{1,2,3} fmt.Println(a) modifySliceData(a) fmt.Println(a) } func modifySliceData(data []int) { data[0] = 0 }
輸出為:
[1 2 3] [0 2 3]
函數modifySliceData
確實通過參數修改了切片的內容.
但是請注意: 修改通過函數修改參數內容的機制有很多, 其中傳參數的地址就可以修改參數的值(其實是修改參數中指針指向的數據), 并不是只有引用一種方式!
傳指針和傳引用是等價的嗎?
比如有以下代碼:
func main() { a := new(int) fmt.Println(a) modify(a) fmt.Println(a) } func modify(a *int) { a = nil }
輸出為:
0xc010000000 0xc010000000
可以看出指針a
本身并沒有變化. 傳指針或傳地址也只能修改指針指向的內存的值, 并不能改變指針本身在值.
因此, 函數參數傳傳指針也是傳值的, 并不是傳引用!
所有類型的函數參數都是傳值的!
包括slice
/map
/chan
等基礎類型和自定義的類型都是傳值的.
但是因為slice
和map
/chan
底層結構的差異, 又導致了它們傳值的影響并不完全等同.
重點歸納如下:
- GoSpec: the parameters of the call are passed by value!
- map/slice/chan 都是傳值, 不是傳引用
- map/chan 對應指針, 和引用類似
slice 是結構體和指針的混合體
slice 含 values/count/capacity 等信息, 是按值傳遞
- slice 中的 values 是指針, 按值傳遞
按值傳遞的 slice 只能修改values指向的數據, 其他都不能修改
以指針或結構體的角度看, 都是值傳遞!
那Go語言有傳引用的說法嗎?
Go語言其實也是有傳引用的地方的, 但是不是函數的參數, 而是閉包對外部環境是通過引用訪問的.
查看以下的代碼:
func main() { a := new(int) fmt.Println(a) func() { a = nil }() fmt.Println(a) }
輸出為:
0xc010000000 <nil>
因為閉包是通過引用的方式使用外部環境的a
變量, 因此可以直接修改a
的值.
比如下面2段代碼的輸出是截然不同的, 原因就是第二個代碼是通過閉包引用的方式輸出i
變量:
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) // Output: 4 3 2 1 0 } fmt.Printf("\n") for i := 0; i < 5; i++ { defer func(){ fmt.Printf("%d ", i) } () // Output: 5 5 5 5 5 }
像第二個代碼就是于閉包引用導致的副作用, 回避這個副作用的辦法是通過參數傳值或每次閉包構造不同的臨時變量:
// 方法1: 每次循環構造一個臨時變量 i for i := 0; i < 5; i++ { i := i defer func(){ fmt.Printf("%d ", i) } () // Output: 4 3 2 1 0 } // 方法2: 通過函數參數傳慘 for i := 0; i < 5; i++ { defer func(i int){ fmt.Printf("%d ", i) } (i) // Output: 4 3 2 1 0 }
總結
- 函數參數傳值, 閉包傳引用!
- slice 含 values/count/capacity 等信息, 是按值傳遞
- 按值傳遞的 slice 只能修改values指向的數據, 其他都不能修改
- slice 是結構體和指針的混合體