閱讀前請悉知:本文是一篇翻譯文章糖荒,出于對原文的喜愛與敬畏带斑,所以需要強調(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
你可以在圖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
可以看到棧上現(xiàn)在有兩個幀,一個是main
须尚,一個increment
崖堤。在increment
的幀中,你可以看到inc
變量耐床,它包含在函數(shù)調(diào)用期間復(fù)制和傳遞的值10密幔。inc
變量的地址是0x10429f98,內(nèi)存更小撩轰,因為幀在棧中是由高地址向低地址擴展的胯甩,不過這只是一個實現(xiàn)細節(jié)昧廷,沒有任何意義。重要的是goroutine
從main
的幀中獲取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
執(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
與圖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
在圖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
以下是輸出數(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)聲明:
任何個人或機構(gòu)如需轉(zhuǎn)載本文歇式,無須再獲得作者書面授權(quán),但是轉(zhuǎn)載者必須保留作者署名糯笙,并注明出處贬丛。
作者保留對本文的修改權(quán)。他人未經(jīng)作者許可给涕,不得擅自修改,破壞作品的完整性额获。
作者保留對本文的其他各項著作權(quán)權(quán)利够庙。