Go語言的傳參和傳引用

傳參和傳引用的問題

不少非官方的文檔和教材(包括一些已經出版的圖書), 對Go語言的傳參和引用的講解 都有不少問題. 致使衆多Go語言新手對Go的函數參數傳參有不少誤解.golang

而傳參和傳引用是編程語言的根本問題, 若是這個問題理解錯誤可能會致使不少問題.編程

傳slice不是傳引用!

首先, Go語言的函數調用參數所有是傳值的, 包括 slice/map/chan 在內全部類型, 沒有傳引用的說法.c#

具體請看Go語言的規範:數組

After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.閉包

from: http://golang.org/ref/spec#Calls編程語言

什麼叫傳引用?

好比有如下代碼:ide

var a Object
doSomething(a) // 修改a的值
print(a)

若是函數doSomething修改a的值, 而後print打印出來的也是修改後的值, 那麼就能夠認爲doSomething是經過引用的方式使用了參數a.函數

爲何傳slice不是傳引用?

咱們構造如下的代碼:lua

func main() {
	a := []int{1,2,3}
	fmt.Println(a)
	modifySlice(a)
	fmt.Println(a)
}

func modifySlice(data []int) {
	data = nil
}

其中modifySlice修改了切片a, 輸出結果以下:.net

[1 2 3]
[1 2 3]

說明a在調用modifySlice先後並無任何變化, 所以a必然是傳值的!

爲何不少人誤覺得slice是傳引用呢?

多是FAQ說slice是引用類型, 但並非傳引用!

下面這個代碼多是錯誤的根源:

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等基礎類型和自定義的類型都是傳值的.

可是由於slicemap/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
}

什麼是引用類型, 和指針有何區別/聯繫 ?

在Go語言的官方FAQ中描述, maps/slices/channels 是引用類型, 數組是值類型:

Why are maps, slices, and channels references while arrays are values?

There's a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

from: http://golang.org/doc/faq#references

我我的理解, 引用類型和指針在底層實現上是同樣的. 可是引用類型在語法上隱藏了顯示的指針操做. 引用類型和函數參數的傳引用/傳值並非一個概念.

咱們知道 maps/slices/channels 在底層雖然隱含了指針, 可是使用中並無須要使用指針的語法. 可是引用內存畢竟是基於指針實現, 所以就必須依賴 make/new 之類的函數才能構造出來. 固然它們都支持字面值語法構造, 可是本質上仍是須要一個構造的過程的.

要用好Go語言的引用類型, 必需要了解一些底層的結構(特別是slice的混合結構).

咱們能夠本身給Go語言模擬一個引用類型. 咱們能夠將值類型特定的數組類型定義爲一個引用類型(同時提供一個構造函數):

type RefIntArray2 *[2]int

func NewRefIntArray2() RefIntArray2 {
	return RefIntArray2(new([2]int))
}

這樣咱們就能夠將 RefIntArray2 看成引用類型來使用.

func main() {
	refArr2 := NewRefIntArray2()
	fmt.Println(refArr2)
	modifyRefArr2(refArr2)
	fmt.Println(refArr2)
}

func modifyRefArr2(arr RefIntArray2) {
	arr[0] = 1
}

輸出爲:

&[0 0]
&[1 0]

之因此選擇數組做爲例子, 是由於Go語言的數組指針能夠直接用[]訪問的語法糖. 因此, 引用類型通常都是底層指針實現, 只是在上層加上的語法糖而已.

注: 本節根據 @hooluupog@LoongWong 的評論作的補充.

總結

  • 函數參數傳值, 閉包傳引用!
  • slice 含 values/count/capacity 等信息, 是按值傳遞
  • 按值傳遞的 slice 只能修改values指向的數據, 其餘都不能修改
  • slice 是結構體和指針的混合體
  • 引用類型和傳引用是兩個概念

https://chai2010.cn/

相關文章
相關標籤/搜索