Go語言的棧和指針機制

閱讀前請悉知:本文是一篇翻譯文章糖荒,出于對原文的喜愛與敬畏带斑,所以需要強調(diào):如果讀者英文閱讀能力好阱州,請直接移步文末原文鏈接挑秉;如果對這篇翻譯所述知識感興趣,也請一定要再看下英文原文苔货,加深理解犀概。翻譯中為了表達的需要,加入了自己的一些理解夜惭,不過因為知識有限姻灶,翻譯過程難免紕漏,如有問題诈茧,歡迎留言指正产喉。

介紹

我不想夸贊指針,因為它很難理解敢会,如若使用不當(dāng)曾沈,極易造成bug, 甚至引發(fā)性能問題,這在編寫并發(fā)或者多線程軟件時鸥昏,顯的尤為突出晦譬。也就難怪許多編程語言都試圖對程序員隱藏指針特性了。但是互广,當(dāng)使用Go編寫軟件敛腌,你是沒有辦法避開指針的。如果對指針沒有深入的理解惫皱,你將很難寫出簡潔高效的代碼像樊。

幀(邊界)

函數(shù)在獨立的內(nèi)存空間(幀)執(zhí)行,而這個獨立的內(nèi)存空間是有邊界的旅敷,這個邊界我們稱之為幀邊界生棍。每個幀都允許函數(shù)在自己的上下文中運行,并提供流量控制(flow control, 暫且這么翻譯)媳谁。
函數(shù)只能直接訪問幀內(nèi)的內(nèi)存涂滴,幀外的內(nèi)存不能間接訪問。如果函數(shù)需要訪問幀外的存儲空間晴音,則該內(nèi)存必須與函數(shù)共享柔纵。為了理解接下來的內(nèi)容,我們需要首先理解幀概念和機制锤躁。(我的理解是:幀是一段有限的供函數(shù)運行的內(nèi)存塊)

當(dāng)一個函數(shù)被調(diào)用搁料,會有兩個幀發(fā)生交互, 即:代碼從調(diào)用函數(shù)的幀轉(zhuǎn)換到被調(diào)用函數(shù)的幀,如果函數(shù)調(diào)用需要傳遞數(shù)據(jù),那么該數(shù)據(jù)必須從一個幀傳遞到另一個幀郭计。在Go中霸琴,數(shù)據(jù)在兩幀之間是按值傳遞的。

按值傳遞的提高了代碼的可讀性昭伸。函數(shù)調(diào)用中數(shù)據(jù)值從一個函數(shù)復(fù)制傳遞梧乘,另一個函數(shù)接收到這個值,整個過程很直觀庐杨,所以你寫代碼時不必為了可讀性而特意掩蓋函數(shù)間交互的過程宋下,因為它就是這么直觀,因而這種直觀可以幫助你理解每個函數(shù)調(diào)用是在如何影響程序運行的辑莫。

Listing 1

01 package main
02
03 func main() {
04
05    // Declare variable of type int with a value of 10.
06    count := 10
07
08    // Display the "value of" and "address of" count.
09    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
10
11    // Pass the "value of" the count.
12    increment(count)
13
14    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc int) {
19
20    // Increment the "value of" inc.
21    inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
23 }

當(dāng)你執(zhí)行以上代碼時学歧,golang中runtime會創(chuàng)建一個主goroutine,這個主goroutine會開始執(zhí)行所有的main函數(shù)內(nèi)所有代碼的初始化各吨。需要明白的是枝笨,goroutine是掛在操作系統(tǒng)線程上的,該線程最終在機器的某個核心上執(zhí)行揭蜒。在1.8版本中横浑,每個goroutine都有一個初始化大小為2048個字節(jié)的連續(xù)內(nèi)存塊,這構(gòu)成了它的堆椞敫空間徙融。這個初始堆棧大小在過去幾年發(fā)生了變化,將來可能會再次發(fā)生變化瑰谜。

棧很重要欺冀,因為它為每個單獨的函數(shù)提供了有限的物理內(nèi)存空間。在主goroutine執(zhí)行清單1中的主函數(shù)時萨脑,goroutine的棧是以下這樣

Figure 1

image.png

你可以在圖1中看到隐轩,棧的一部分已經(jīng)被主函數(shù)所占據(jù),即main所屬的內(nèi)存幀(幀在棧上分配的)渤早,這個方框表示棧上的主函數(shù)邊界职车。幀的范圍作為調(diào)用函數(shù)時執(zhí)行的代碼的一部分建立。你還可以看到count變量的內(nèi)存已經(jīng)放在main所在幀的地址0x10429fa4上鹊杖。

圖1還說明了另一個有趣的問題悴灵。活動幀以下的所有棧內(nèi)存都無效骂蓖,但活動幀以上的棧內(nèi)存是有效的积瞒。我需要明確幀的有效部分和無效部分之間的界限(是否有被使用)。

地址

變量的作用是特定的內(nèi)存位置賦予名字涯竟,以提高代碼的可讀性赡鲜,并幫助你分析正在使用的數(shù)據(jù)空厌。如果你有一個變量庐船,那么對應(yīng)內(nèi)存中一個值银酬,如果內(nèi)存中有一個值,那么它必須有一個地址筐钟。在第09行揩瞪,主函數(shù)調(diào)用內(nèi)置函數(shù)println來顯示count變量的“值”和“地址”。

Listing 2

09    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")

使用&運算符來獲取變量位置的地址并不新奇篓冲,其它語言也使用這個運算符李破。第09行的輸出應(yīng)該類似于下面的輸出,如果你在一個32位架構(gòu)(如游樂場)上運行代碼:

Listing 3

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

函數(shù)調(diào)用

在第12行上壹将,main函數(shù)調(diào)用了increment函數(shù)嗤攻。

Listing 4

12    increment(count)

調(diào)用函數(shù)意味著goroutine需要在棧上開辟一個新的內(nèi)存空間。然而事情要可能比你想像的還要復(fù)雜一些哦诽俯。要成功地進行此函數(shù)調(diào)用妇菱,需要在轉(zhuǎn)換過程中在兩個幀之間傳遞數(shù)據(jù)。具體地說暴区,一個整數(shù)值將在調(diào)用期間被復(fù)制和傳遞闯团。通過查看第18行上的increment函數(shù)的聲明,你可以看到這一點仙粱。

Listing 5

18 func increment(inc int) {

如果你在第12行再次看到遞增的函數(shù)調(diào)用房交,你會看到代碼正在傳遞count變量的“值”。該值將被復(fù)制伐割、傳遞給increment函數(shù)所在的幀中候味。記住,increment函數(shù)只能在它自己的空間內(nèi)直接讀寫內(nèi)存隔心,因此它需要inc變量接收负溪、存儲和訪問它自己傳遞的count值的副本。

increment函數(shù)內(nèi)部的代碼開始執(zhí)行之前济炎,goroutine的棿眨看起來是這樣的:
Figure 2

image.png

可以看到棧上現(xiàn)在有兩個幀,一個是main须尚,一個increment崖堤。在increment的幀中,你可以看到inc變量耐床,它包含在函數(shù)調(diào)用期間復(fù)制和傳遞的值10密幔。inc變量的地址是0x10429f98,內(nèi)存更小撩轰,因為幀在棧中是由高地址向低地址擴展的胯甩,不過這只是一個實現(xiàn)細節(jié)昧廷,沒有任何意義。重要的是goroutinemain的幀中獲取count的值偎箫,并使用inc變量在幀中存儲了該值的副本木柬。

increment函數(shù)中的其余代碼顯示inc變量的“值”和“地址”贱呐。

Listing 6

21    inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")

22行的輸出如下

Listing 7

inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]

這是在執(zhí)行到第22行后棧的樣子:

Figure 3

image.png

執(zhí)行第21和22行之后圣蝎,increment函數(shù)返回到main函數(shù)看铆。然后主函數(shù)在第14行再次count變量的“值”和“地址”彼妻。

Listing 8

14    println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")

輸出如下如示

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]
count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

main所在幀中count的值在調(diào)用increment前后相同饵隙。

函數(shù)返回

當(dāng)一個函數(shù)返回到調(diào)用方函數(shù)時嘿悬,棧上的內(nèi)存實際發(fā)生了什么?其實什么都沒有恐疲。這是increment函數(shù)執(zhí)行完成返回后棧的樣子:

Figure 4

image.png

與圖3幾乎完全相同漱凝,只是increment函數(shù)關(guān)聯(lián)的幀現(xiàn)在被認為是無效內(nèi)存愕乎。這是因為main的幀現(xiàn)在是活動幀。increment函數(shù)所在的幀在內(nèi)存中保持不變,此時它是非活動幀。

清理返回函數(shù)的幀的內(nèi)存會浪費時間,因為你不知道是否還需要這個內(nèi)存撮竿。所以內(nèi)存就保持原樣了。在每次函數(shù)調(diào)用期間授账,在獲取幀時,該幀的棧內(nèi)存將被清除攻臀。這是通過初始化放置在幀中的任何值來完成的堡赔。因為所有的值都被初始化為至少它們的“零值”离例,所以棧在每次函數(shù)調(diào)用時都會自動清理。
(這里我理解因為每個幀其實是有邊界的,程序運行時知道此時幀的邊界在哪里,比如若此時main調(diào)用另一個函數(shù)increment2,可能會占據(jù)原increment的幀,完成初始化,相當(dāng)于是覆蓋了)

共享

有什么辦法能讓increment函數(shù)直接操作main的幀中存在的count變量呢?答案是指針婿崭。指針的存在只有一個目的授瘦,即與函數(shù)共享一個值形纺,以便函數(shù)可以讀寫該值,即使該值并不直接存在于其自身的幀中徒欣。

如果你不知道共享逐样,你就不需要使用指針。學(xué)習(xí)指針時帚称,重要的是要使用清晰的詞匯表官研,而不是操作符或語法。所以請記住闯睹,指針是用于共享的戏羽,并在你讀取代碼時將&操作符替換為“共享”。

指針類型

Go有許多內(nèi)置類型楼吃, 這些內(nèi)置類型都能很方便的聲明為指針類型始花。比如已經(jīng)存在一個名為int的內(nèi)置類型,因此有一個指針類型稱為*int孩锡。如果聲明了一個名為User的類型酷宵,就可以獲得一個名為*User的指針類型。

所有指針類型都具有相同的兩個特征躬窜。首先浇垦,他們從角色*開始。其次荣挨,它們都具有相同的內(nèi)存大小和表示形式男韧,即表示地址的4或8字節(jié)朴摊。在32位架構(gòu)上,指針需要4字節(jié)的內(nèi)存此虑,而在64位架構(gòu)(如你的機器)上甚纲,它們需要8字節(jié)的內(nèi)存。

間接訪問內(nèi)存

看看這個小程序朦前,它執(zhí)行一個函數(shù)調(diào)用介杆,通過按值傳遞地址。這將與increment函數(shù)共享main的幀中的count變量

Listing 10

01 package main
02
03 func main() {
04
05    // Declare variable of type int with a value of 10.
06    count := 10
07
08    // Display the "value of" and "address of" count.
09    println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
10
11    // Pass the "address of" count.
12    increment(&count)
13
14    println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc *int) {
19
20    // Increment the "value of" count that the "pointer points to". (dereferencing)
21    *inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]")
23 }

從最初的程序中韭寸,有三個有趣的變化春哨。這是第12行上的第一個變化

Listing 11

12    increment(&count)

這一次在第12行,代碼不是復(fù)制和傳遞計數(shù)的“值”棒仍,而是傳遞計數(shù)的“地址””ィ現(xiàn)在你可以說臭胜,我正在與increment函數(shù)“共享”count變量莫其。這就是&操作符功能:“共享”。

請理解這仍然是一個“按值傳遞”耸三,唯一的區(qū)別是你傳遞的值是一個地址而不是整數(shù)乱陡。地址也是值; 這是正在復(fù)制并通過幀傳遞給函數(shù)調(diào)用者的內(nèi)容。

由于正在復(fù)制和傳遞地址的值仪壮,因此需要在increment幀內(nèi)設(shè)置一個變量來接收和存儲這個整數(shù)的地址憨颠。這就是整型指針變量的聲明在第18行出現(xiàn)的地方。

Listing 12

18 func increment(inc *int) {

如果要傳遞User值的地址积锅,則需要將變量聲明為*User爽彤。即使所有指針變量都存儲地址值,它們也不能傳遞任何地址缚陷,只能傳遞與指針類型關(guān)聯(lián)的地址适篙。這是關(guān)鍵,共享一個值的原因是因為接收函數(shù)需要對該值執(zhí)行讀寫操作箫爷。你需要任何值的類型信息才能對其進行讀寫嚷节。編譯器將確保只有與正確指針類型關(guān)聯(lián)的值與該函數(shù)共享。

這是函數(shù)調(diào)用increment后棧的樣子:

Figure 5

image.png

在圖5中可以看到虎锚,當(dāng)使用地址作為值執(zhí)行“傳遞值”時硫痰,棧是什么樣子的。increment函數(shù)幀的指針變量現(xiàn)在指向count變量窜护,它位于main所在的幀內(nèi)效斑。

現(xiàn)在使用指針變量,函數(shù)可以對main幀內(nèi)的count變量執(zhí)行間接的讀修改寫操作柱徙。

Listing 13

21    *inc++

這一次缓屠,字符充當(dāng)操作符并應(yīng)用于指針變量税娜。使用作為運算符意味著,“指針指向的值”藏研。指針變量允許在使用它的函數(shù)幀之外間接訪問內(nèi)存敬矩。有時這種間接的讀或?qū)懕环Q為取消指針引用。increment函數(shù)在它的幀內(nèi)仍然必須有一個指針變量蠢挡,它可以直接讀取來執(zhí)行間接訪問弧岳。

現(xiàn)在,在圖6中业踏,你可以看到第21行執(zhí)行之后的棧是什么樣子的禽炬。

Figure 6

image.png

以下是輸出數(shù)據(jù)

Listing 14

count:  Value Of[ 10 ]          Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 0x10429fa4 ]  Addr Of[ 0x10429f98 ]   Value Points To[ 11 ]
count:  Value Of[ 11 ]          Addr Of[ 0x10429fa4 ]

你可以看到,inc指針變量的“值”與計數(shù)變量的“地址”相同勤家。這樣就建立了共享關(guān)系腹尖,允許對幀之外的內(nèi)存進行間接訪問。當(dāng)increment函數(shù)通過指針執(zhí)行寫操作時伐脖,main函數(shù)會在返回時看到更改热幔。

指針沒什么特別

指針變量并不特殊,因為它與其他變量一樣都只是變量而已讼庇。它們有一個內(nèi)存分配和一個值绎巨。所有的指針變量,不管它們指向的值是什么類型蠕啄,大小和表示方式都是一樣的场勤。令人困惑的是*字符在代碼中充當(dāng)操作符,用于聲明指針類型歼跟。

總結(jié)

這篇文章描述了指針背后的目的和媳,以及棧和指針機制如何在Go中工作,如果你理解了這種設(shè)計的理念與機制哈街,恭喜你留瞳,在編寫簡潔高效代碼的的旅途中,你邁出了第一步叹卷。

總之撼港,看到這里,你可以學(xué)到許多:

  • 函數(shù)在幀邊界范圍內(nèi)執(zhí)行骤竹,幀為每個單獨的函數(shù)提供單獨的內(nèi)存空間帝牡。
  • 當(dāng)調(diào)用一個函數(shù)時,在兩個幀之間會產(chǎn)生交互蒙揣。
  • 按值傳遞數(shù)據(jù)的好處是可讀性靶溜。
  • 棧很重要,因為它為每個單獨的函數(shù)提供了有邊界的物理內(nèi)存空間。
  • 活動幀以下的所有棧內(nèi)存都無效罩息,但活動幀以上的內(nèi)存是有效的嗤详。
  • 調(diào)用函數(shù)意味著goroutine需要在堆棧上開辟一段新的內(nèi)存空間。
  • 在每次函數(shù)調(diào)用期間瓷炮,在獲取幀時葱色,該幀的堆棧內(nèi)存將被清除(覆蓋)。
  • 指針的意義娘香,即與函數(shù)共享一個值苍狰,以便函數(shù)可以讀寫該值,即使該值并不直接存在于其所在幀中烘绽。
  • 對于由你或語言本身聲明的每一種類型淋昭,你都可以免費獲得用于共享的恭維指針類型。
  • 指針變量允許在使用它的函數(shù)的幀之外間接訪問內(nèi)存安接。
  • 指針變量并不特殊翔忽,因為它們和其他變量一樣都是變量。它們有一個內(nèi)存分配和一個值盏檐。

版權(quán)聲明:

  1. 任何個人或機構(gòu)如需轉(zhuǎn)載本文歇式,無須再獲得作者書面授權(quán),但是轉(zhuǎn)載者必須保留作者署名糯笙,并注明出處贬丛。

  2. 作者保留對本文的修改權(quán)。他人未經(jīng)作者許可给涕,不得擅自修改,破壞作品的完整性额获。

  3. 作者保留對本文的其他各項著作權(quán)權(quán)利够庙。

原文閱讀:
Language Mechanics On Stacks And Pointers

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抄邀,隨后出現(xiàn)的幾起案子耘眨,更是在濱河造成了極大的恐慌,老刑警劉巖境肾,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剔难,死亡現(xiàn)場離奇詭異,居然都是意外死亡奥喻,警方通過查閱死者的電腦和手機偶宫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來环鲤,“玉大人纯趋,你說我怎么就攤上這事。” “怎么了吵冒?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵纯命,是天一觀的道長。 經(jīng)常有香客問我痹栖,道長亿汞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任揪阿,我火速辦了婚禮留夜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘图甜。我一直安慰自己碍粥,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布黑毅。 她就那樣靜靜地躺著嚼摩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪矿瘦。 梳的紋絲不亂的頭發(fā)上枕面,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音缚去,去河邊找鬼潮秘。 笑死,一個胖子當(dāng)著我的面吹牛易结,可吹牛的內(nèi)容都是我干的枕荞。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼搞动,長吁一口氣:“原來是場噩夢啊……” “哼躏精!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鹦肿,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤矗烛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后箩溃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞭吃,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年涣旨,在試婚紗的時候發(fā)現(xiàn)自己被綠了歪架。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡开泽,死狀恐怖牡拇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤惠呼,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布导俘,位于F島的核電站,受9級特大地震影響剔蹋,放射性物質(zhì)發(fā)生泄漏旅薄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一泣崩、第九天 我趴在偏房一處隱蔽的房頂上張望少梁。 院中可真熱鬧,春花似錦矫付、人聲如沸凯沪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妨马。三九已至,卻和暖如春杀赢,著一層夾襖步出監(jiān)牢的瞬間烘跺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工脂崔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留滤淳,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓砌左,卻偏偏與公主長得像脖咐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子绊困,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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