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

今天云稚,嘗試談下 Go 中的引用隧饼。

之所以要談它,一方面是之前的我也有些概念混亂静陈,想梳理下燕雁,另一方面是因為很多人對引用都有疑問。我經(jīng)常會看到與引用有關的問題窿给。

比如贵白,什么是引用?引用和指針有什么區(qū)別崩泡?Go 中有引用類型嗎禁荒?什么是值傳遞?址傳遞角撞?引用傳遞呛伴?

在開始談論之前勃痴,我已經(jīng)感覺到這必定是一個非常頭疼的話題。這或許就是學了那么多語言热康,但沒有深入總結沛申,從而導致的思維混亂。

前言

我的理解是姐军,要徹底搞懂引用铁材,得從類型和傳遞兩個角度分別進行思考。

從類型角度奕锌,類型可分為值類型和引用類型著觉,一般而言,我們說到引用惊暴,強調的都是類型饼丘。

從傳遞角度,有值傳遞辽话、址傳遞和引用傳遞肄鸽,傳遞是在函數(shù)調用時才會提到的概念,用于表明實參與形參的關系油啤。

引用類型和引用傳遞的關系典徘,我嘗試用一句話概括,引用類型不一定是引用傳遞村砂,但引用傳遞的一定是引用類型烂斋。

這幾句話,是我在使用各種語言的之后總結出來的础废,希望無誤吧汛骂,畢竟不能誤導他人。

是什么

談到引用评腺,就不得不提指針帘瞭,而指針與引用是編程學習中老生常談的話題了。有些編程語言為了降低程序員的使用門檻蒿讥,只有引用蝶念。而有些語言則是指針引用皆存在,如 C++ 和 Go芋绸。

指針媒殉,即地址的意思。

在程序運行的時候摔敛,操作系統(tǒng)會為每個變量分配一塊內(nèi)存放變量內(nèi)容廷蓉,而這塊內(nèi)存有一個編號,即內(nèi)存地址马昙,也就是變量的地址√胰現(xiàn)在 CPU 一般都是 64 位刹悴,因而,這個地址的長度一般也就是 8 個字節(jié)攒暇。

引用土匀,某塊內(nèi)存的別名。

一般情況形用,都會這么解釋引用就轧。換句話說,引用代指某個內(nèi)存地址田度,這句話真的是非常簡潔钓丰,同時也非常好理解。但在 Go 中每币,這句話看起來并不全面,具體后面解釋琢歇。

除了指針和引用兰怠,還有另外一個更廣泛的概念,值李茫。談變量傳遞時揭保,常會提到值傳遞、址傳遞和引用傳遞魄宏。從廣義上看秸侣,對大部分的語言而言,指針和引用都屬于值宠互。而從狹義角度來說味榛,則可分為值、址和引用予跌。

相當繞人是不是搏色?

我已經(jīng)感覺到自己頭發(fā)在掉了。其實券册,要想徹底搞清楚這些概念频轿,還是得從本質出發(fā)。

值和指針

先來搞明白值與指針區(qū)別烁焙。

上一節(jié)在介紹指針的時候航邢,提到了要注意變量的地址和內(nèi)容的不同。為什么要說這句話呢骄蝇?

假設膳殷,我們定義一個 int 類型的變量 a,如下:

var a int = 1

變量 a 的內(nèi)容為 1乞榨,而變量內(nèi)容是存在某個地址之中的秽之。如何獲取變量地址呢当娱?Go 中獲取變量地址的方法與 C/C++ 相同淘太。代碼如下:

var p = &a

通過 & 獲取 a 的地址皇钞。同時,這里還定義了一個新的變量 p 用于保存變量 a 的地址躺酒。p 的類型為 int 指針河质,也就是變量 p 中的內(nèi)容是變量 a 的地址冀惭。

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

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

我這里的輸出結果是,變量 a 和 p 的地址分別為 0xc000092000 和 0xc00008c010掀鹅。此時的內(nèi)存的分布如下:

[圖片上傳失敗...(image-b1d048-1569668039616)]

變量 p 的內(nèi)容是 a 的地址散休,因而可以說指針即是其他變量的內(nèi)容,也是某個變量的地址乐尊。為什么啰啰嗦嗦的說這些戚丸,因為在學習 C 語言,會單獨強調址的概念扔嵌,但在 Go 中限府,指針相對弱化,也是歸于值類型之中痢缎。

引用的本質

前面說過胁勺,引用是某塊內(nèi)存的別名。從字面理解独旷,似乎表達的是引用類型變量中的內(nèi)容是指針署穗,這么理解似乎也沒錯。既然如此嵌洼,我自然而然地想到案疲,怎么將引用與指針關聯(lián)起來。

在 C/C++ 中麻养,引用其實是編譯器實現(xiàn)的一個語法糖络拌,經(jīng)過匯編后,將會把引用操作轉化為了指針操作回溺。這真的是別名啊春贸,有種 define 預處理的感覺,只不過是匯編級別的遗遵。分享一篇 C++中“引用”的底層實現(xiàn) 的文章萍恕,有興趣仔細讀讀,我只是看了個大概车要。

而其他一些語言中允粤,引用的本質其實是 struct 中包含指針,比如 Python。下面的 C 結構是 Python 中列表類型的底層結構类垫。

typedef struct {
    PyObject_VAR_HEAD

    PyObject **ob_item;

    Py_ssize_t allocated;
} PyListObject;

變量真正存放數(shù)據(jù)的地方在 **ob_item 中司光。結構中的其他兩個成員起輔助作用。

現(xiàn)在看來悉患,引用的實現(xiàn)主要有兩種残家。一是 C++ 的思路,引用其實一種便于使用指針的語法糖售躁,和我們想象中的別名含義一致坞淮。二是類似 Python 中的實現(xiàn),底層結構中包含指向實際內(nèi)容的指針陪捷。

當然回窘,或許還有其他的實現(xiàn)方式,但核心應該是不變的市袖。

引用傳遞

談到引用傳遞啡直,就不得不提值傳遞,值傳遞的一般定義如下苍碟。

函數(shù)調用時付枫,實參通過拷貝將自身內(nèi)容傳遞給形參,形參實際上是實參值的一個拷貝驰怎,此時,針對函數(shù)中形參的任何操作二打,僅僅是針對實參的副本县忌,不影響原始值的內(nèi)容。

值傳遞中有一個特殊形式继效,如果傳遞參數(shù)的類型是指針症杏,我們就會稱之為址傳遞,C 語言中就有值傳遞和址傳遞兩種說法瑞信。深究起來厉颤,C 中的址傳遞也屬于值傳遞,因為對指針類型而言凡简,變量的值是指針逼友,即傳遞的值也是指針。而 C 語言之所以強調址傳遞秤涩,我認為主要 C 這門底層語言對指針較為重視帜乞。

什么是引用傳遞?

參考值傳遞的定義筐眷,實參地址在函數(shù)調用被傳遞給形參黎烈,針對形參的操作,影響到了實參,則可以認為是引用傳遞照棋。

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

Go 的引用實現(xiàn)

Go 的引用類型有 slice烈炭、map 和 chan溶锭,實現(xiàn)機制采用的是前面提到的第二種方式,即結構體含指針成員梳庆。它們都可以使用內(nèi)置函數(shù) make 進行初始化暖途。

原本我是想把這幾種引用類型的底層結構都貼出來,但發(fā)現(xiàn)這會干擾本文主題的理解膏执。我們只看 slice 的結構驻售,如下:

// slice
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

slice 的結構最簡單,包含三個成員更米,分別是切片的底層數(shù)組地址欺栗、切片長度和容量大小。是否感覺與前面提到的 Python 列表的底層結構非常類似征峦?

如果想了解 map 和 chan 的結構迟几,可自行閱讀 go 的源碼,runtime/slice.go栏笆、runtime/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 中沒有址傳遞

其實巢块,這個問題前面已經(jīng)解釋的很清楚了,指針只是值的一種特殊形式巧号,C 語言是門非常底層的語言族奢,常會涉及一些地址操作,會強調指針的特殊地位丹鸿。但于 Go 而言越走,指針已經(jīng)弱化了很多,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;
}

執(zhí)行結果如下:

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 引用的實現(xiàn)機制不同正卧,理解起來會比較困難。我們也可以選擇只記結論跪解。

這種方式的驗證非常簡單炉旷,我們在上面的 C++ 和 Go 的例子中已經(jīng)輸出了形參和實參的地址,比較下即可得出結論。

總結

本文主要從引用的類型和傳遞兩個角度出發(fā)窘行,深入淺出的分析了 Go 中的引用饥追。

首先,引用類型和引用傳遞并沒有絕對的關系罐盔,不知道有多少人認為引用類型必然是引用傳遞但绕。接著,我們討論了不同語言引用的實現(xiàn)機制惶看,涉及到 C++捏顺、Python 和 Go。

文章的最后纬黎,解釋了一個常見的疑惑幅骄,為什么說 Go 只有值傳遞。在此基礎上莹桅,文中提出了兩種方式昌执,幫助識別一門語言是否支持引用傳遞。

相關閱讀

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

Golang中的make(T, args)為什么返回T而不是*T?

Go語言參數(shù)傳遞是傳值還是傳引用

Golang中函數(shù)傳參存在引用傳遞嗎懂拾?

C++ 引用 底層實現(xiàn)機制

The Go Programming Language Specification

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市铐达,隨后出現(xiàn)的幾起案子岖赋,更是在濱河造成了極大的恐慌,老刑警劉巖瓮孙,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唐断,死亡現(xiàn)場離奇詭異,居然都是意外死亡杭抠,警方通過查閱死者的電腦和手機脸甘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偏灿,“玉大人丹诀,你說我怎么就攤上這事∥檀梗” “怎么了铆遭?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沿猜。 經(jīng)常有香客問我枚荣,道長,這世上最難降的妖魔是什么啼肩? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任橄妆,我火速辦了婚禮衙伶,結果婚禮上,老公的妹妹穿的比我還像新娘呼畸。我一直安慰自己痕支,他們只是感情好,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布蛮原。 她就那樣靜靜地躺著卧须,像睡著了一般。 火紅的嫁衣襯著肌膚如雪儒陨。 梳的紋絲不亂的頭發(fā)上花嘶,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音蹦漠,去河邊找鬼椭员。 笑死,一個胖子當著我的面吹牛笛园,可吹牛的內(nèi)容都是我干的隘击。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼研铆,長吁一口氣:“原來是場噩夢啊……” “哼埋同!你這毒婦竟也來了?” 一聲冷哼從身側響起棵红,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤凶赁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后逆甜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虱肄,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年交煞,在試婚紗的時候發(fā)現(xiàn)自己被綠了咏窿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡素征,死狀恐怖集嵌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稚茅,我是刑警寧澤纸淮,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布平斩,位于F島的核電站亚享,受9級特大地震影響,放射性物質發(fā)生泄漏绘面。R本人自食惡果不足惜欺税,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一侈沪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晚凿,春花似錦亭罪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至燥筷,卻和暖如春箩祥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肆氓。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工袍祖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谢揪。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓蕉陋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拨扶。 傳聞我的和親對象是個殘疾皇子凳鬓,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容