一文理清 Go 引用的常見疑惑

本文首發於個人博客,若是有用,歡迎點贊收藏,讓更多的朋友看到。html

今天,嘗試談下 Go 中的引用。git

之因此要談它,一方面是以前的我也有些概念混亂,想梳理下,另外一方面是由於不少人對引用都有疑問。我常常會看到與引用有關的問題。程序員

好比,什麼是引用?引用和指針有什麼區別?Go 中有引用類型嗎?什麼是值傳遞?址傳遞?引用傳遞?github

在開始談論以前,我已經感受到這一定是一個很是頭疼的話題。這或許就是學了那麼多語言,但沒有深刻總結,從而致使的思惟混亂。golang

前言

個人理解是,要完全搞懂引用,得從類型和傳遞兩個角度分別進行思考。編程

從類型角度,類型可分爲值類型和引用類型,通常而言,咱們說到引用,強調的都是類型。c#

從傳遞角度,有值傳遞、址傳遞和引用傳遞,傳遞是在函數調用時纔會提到的概念,用於代表實參與形參的關係。segmentfault

引用類型和引用傳遞的關係,我嘗試用一句話歸納,引用類型不必定是引用傳遞,但引用傳遞的必定是引用類型。數組

這幾句話,是我在使用各類語言的以後總結出來的,但願無誤吧,畢竟不能誤導他人。bash

是什麼

談到引用,就不得不提指針,而指針與引用是編程學習中老生常談的話題了。有些編程語言爲了下降程序員的使用門檻,只有引用。而有些語言則是指針引用皆存在,如 C++ 和 Go。

指針,即地址的意思。

在程序運行的時候,操做系統會爲每一個變量分配一塊內存放變量內容,而這塊內存有一個編號,即內存地址,也就是變量的地址。如今 CPU 通常都是 64 位,於是,這個地址的長度通常也就是 8 個字節。

引用,某塊內存的別名。

通常狀況,都會這麼解釋引用。換句話說,引用代指某個內存地址,這句話真的是很是簡潔,同時也很是好理解。但在 Go 中,這句話看起來並不全面,具體後面解釋。

除了指針和引用,還有另一個更普遍的概念,值。談變量傳遞時,常會提到值傳遞、址傳遞和引用傳遞。從廣義上看,對大部分的語言而言,指針和引用都屬於值。而從狹義角度來講,則可分爲值、址和引用。

至關繞人是否是?

我已經感受到本身頭髮在掉了。其實,要想完全搞清楚這些概念,仍是得從本質出發。

值和指針

先來搞明白值與指針區別。

上一節在介紹指針的時候,提到了要注意變量的地址和內容的不一樣。爲何要說這句話呢?

假設,咱們定義一個 int 類型的變量 a,以下:

var a int = 1
複製代碼

變量 a 的內容爲 1,而變量內容是存在某個地址之中的。如何獲取變量地址呢?Go 中獲取變量地址的方法與 C/C++ 相同。代碼以下:

var p = &a
複製代碼

經過 & 獲取 a 的地址。同時,這裏還定義了一個新的變量 p 用於保存變量 a 的地址。p 的類型爲 int 指針,也就是變量 p 中的內容是變量 a 的地址。

以下代碼輸出它們的地址:

var a = 1
var p = &a
fmt.Printf("%p\n", p)
fmt.Printf("%p\n", &p)
複製代碼

我這裏的輸出結果是,變量 a 和 p 的地址分別爲 0xc000092000 和 0xc00008c010。此時的內存的分佈以下:

變量 p 的內容是 a 的地址,於是能夠說指針便是其餘變量的內容,也是某個變量的地址。爲何囉囉嗦嗦的說這些,由於在學習 C 語言,會單獨強調址的概念,但在 Go 中,指針相對弱化,也是歸於值類型之中。

引用的本質

前面說過,引用是某塊內存的別名。從字面理解,彷佛表達的是引用類型變量中的內容是指針,這麼理解彷佛也沒錯。既然如此,我天然而然地想到,怎麼將引用與指針關聯起來。

在 C/C++ 中,引用實際上是編譯器實現的一個語法糖,通過彙編後,將會把引用操做轉化爲了指針操做。這真的是別名啊,有種 define 預處理的感受,只不過是彙編級別的。分享一篇 C++中「引用」的底層實現 的文章,有興趣仔細讀讀,我只是看了個大概。

而其餘一些語言中,引用的本質實際上是 struct 中包含指針,好比 Python。下面的 C 結構是 Python 中列表類型的底層結構。

typedef struct {
    PyObject_VAR_HEAD

    PyObject **ob_item;

    Py_ssize_t allocated;
} PyListObject;
複製代碼

變量真正存放數據的地方在 **ob_item 中。結構中的其餘兩個成員起輔助做用。

如今看來,引用的實現主要有兩種。一是 C++ 的思路,引用其實一種便於使用指針的語法糖,和咱們想象中的別名含義一致。二是相似 Python 中的實現,底層結構中包含指向實際內容的指針。

固然,或許還有其餘的實現方式,但核心應該是不變的。

引用傳遞

談到引用傳遞,就不得不提值傳遞,值傳遞的通常定義以下。

函數調用時,實參經過拷貝將自身內容傳遞給形參,形參其實是實參值的一個拷貝,此時,針對函數中形參的任何操做,僅僅是針對實參的副本,不影響原始值的內容。

值傳遞中有一個特殊形式,若是傳遞參數的類型是指針,咱們就會稱之爲址傳遞,C 語言中就有值傳遞和址傳遞兩種說法。深究起來,C 中的址傳遞也屬於值傳遞,由於對指針類型而言,變量的值是指針,即傳遞的值也是指針。而 C 語言之因此強調址傳遞,我認爲主要 C 這門底層語言對指針較爲重視。

什麼是引用傳遞?

參考值傳遞的定義,實參地址在函數調用被傳遞給形參,針對形參的操做,影響到了實參,則能夠認爲是引用傳遞。

在我用過的語言中,支持引用傳遞的語言有 PHP 和 C++。

Go 的引用實現

Go 的引用類型有 slice、map 和 chan,實現機制採用的是前面提到的第二種方式,即結構體含指針成員。它們均可以使用內置函數 make 進行初始化。

本來我是想把這幾種引用類型的底層結構都貼出來,但發現這會干擾本文主題的理解。咱們只看 slice 的結構,以下:

// slice
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
複製代碼

slice 的結構最簡單,包含三個成員,分別是切片的底層數組地址、切片長度和容量大小。是否感受與前面提到的 Python 列表的底層結構很是相似?

若是想了解 map 和 chan 的結構,可自行閱讀 go 的源碼,runtime/slice.goruntime/map.goruntime/chan.go

若是不想研究源碼,推薦閱讀饒大的 Go 深度解密系列文章,包括 深度解密Go語言之Slice深度解密Go語言之map深度解密Go語言之channel,這幾篇文章由於寫的都很是細且很是長,可能讀起來會比較考驗你的耐心。

Go 是值傳遞

按官方說法,Go 中只有值傳遞。原文以下:

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.

重點是下面這句話。

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

有點迷糊?最初我也迷糊,Go 不是有指針和引用類型嗎。但讀了一些文章,思考了許久,才完全想明白。下面,我將嘗試爲官方的說法找個合理的解釋。

爲何說 Go 中沒有址傳遞

其實,這個問題前面已經解釋的很清楚了,指針只是值的一種特殊形式,C 語言是門很是底層的語言,常會涉及一些地址操做,會強調指針的特殊地位。但於 Go 而言,指針已經弱化了不少,Go 團隊可能也以爲沒有必要再單獨強調指針的地位。

爲何說 Go 中沒有引用傳遞?

有人可能會說,Go 中明明有引用傳遞,按照引用傳遞的定義,能夠很是容易就拿出一個例子反駁我。

package main

import "fmt"

func update(s []int) {
	s[1] = 10
}

func main() {
	a := []int{0, 1, 2, 3, 4}
	fmt.Println(a)
	update(a)
	fmt.Println(a)
}
複製代碼

輸出結果以下:

[0 1 2 3 4]
[0 10 2 3 4]
複製代碼

針對形參 s 的操做確實改變了實參 a 的值,彷佛的確是引用傳遞。但我想說的是,針對形參的操做並不是指的是針對形參中某個元素的操做。

看個 C++ 中引用的例子。

void update(int& s) {
	s = 10;
	printf("s address: %p\n", &s);
}

int main() {
	int a = 1;
	std::cout << a << std::endl;
	printf("a address: %p\n", &a);
	update(a);
	std::cout << a << std::endl;
}
複製代碼

執行結果以下:

1
a address: 0x7fff5b98f21c
s address: 0x7fff5b98f21c
10
複製代碼

針對 s 的操做確實改變了 a 的值。在 Go 中嘗試一樣的代碼,以下:

func update(s []int) {
	s[1] = 10
	fmt.Printf("%p\n", &s)
}

func main() {
	a := []int{0, 1, 2, 3, 4}
	fmt.Println(a)
	fmt.Printf("%p\n", &a)
	update(a)
	fmt.Println(a)
}
複製代碼

輸出以下:

[0 1 2 3 4]
0xc00000c060
0xc000098000
[0 10 2 3 4]
複製代碼

很是遺憾,針對形參的賦值操做並無改變實參的值。基於此,得出結論是 slice 的傳遞並不是引用傳遞。我比較喜歡的這種解釋方式,適合我我的的記憶理解,不知道是否有不妥的地方。

除此以外,介紹另一種識別是不是引用傳遞的方式。

經過比較形參和實參地址確認,若是二者地址相同,則是引用傳遞,不一樣則非引用傳遞。但由於 C++ 和 Go 引用的實現機制不一樣,理解起來會比較困難。咱們也能夠選擇只記結論。

這種方式的驗證很是簡單,咱們在上面的 C++ 和 Go 的例子中已經輸出了形參和實參的地址,比較下便可得出結論。

總結

本文主要從引用的類型和傳遞兩個角度出發,深刻淺出的分析了 Go 中的引用。

首先,引用類型和引用傳遞並無絕對的關係,不知道有多少人認爲引用類型必然是引用傳遞。接着,咱們討論了不一樣語言引用的實現機制,涉及到 C++、Python 和 Go。

文章的最後,解釋了一個常見的疑惑,爲何說 Go 只有值傳遞。在此基礎上,文中提出了兩種方式,幫助識別一門語言是否支持引用傳遞。

相關閱讀

golang中哪些引用類型的指針在聲明時不用加&號,哪些在函數定義的形參和返回值類型中不用*號標註

Golang中的make(T, args)爲何返回T而不是*T?

Go語言參數傳遞是傳值仍是傳引用

Golang中函數傳參存在引用傳遞嗎?

C++ 引用 底層實現機制

The Go Programming Language Specification


歡迎關注個人公衆號。

相關文章
相關標籤/搜索