不像 Java 和 .NET,Go 語言為程序員提供了控制數(shù)據(jù)結(jié)構(gòu)的指針的能力;但是,你不能進(jìn)行指針運(yùn)算。通過給予程序員基本內(nèi)存布局,Go 語言允許你控制特定集合的數(shù)據(jù)結(jié)構(gòu)、分配的數(shù)量以及內(nèi)存訪問模式,這些對(duì)構(gòu)建運(yùn)行良好的系統(tǒng)是非常重要的:指針對(duì)于性能的影響是不言而喻的,而如果你想要做的是系統(tǒng)編程、操作系統(tǒng)或者網(wǎng)絡(luò)應(yīng)用,指針更是不可或缺的一部分。
由于各種原因,指針對(duì)于使用面向?qū)ο缶幊痰默F(xiàn)代程序員來說可能顯得有些陌生,不過我們將會(huì)在這一小節(jié)對(duì)此進(jìn)行解釋,并在未來的章節(jié)中展開深入討論。
程序在內(nèi)存中存儲(chǔ)它的值,每個(gè)內(nèi)存塊(或字)有一個(gè)地址,通常用十六進(jìn)制數(shù)表示,如:0x6b0820 或 0xf84001d7f0。
Go 語言的取地址符是 &,放到一個(gè)變量前使用就會(huì)返回相應(yīng)變量的內(nèi)存地址。
下面的代碼片段(示例 4.9 pointer.go)可能輸出 An integer: 5, its location in memory: 0x6b0820(這個(gè)值隨著你每次運(yùn)行程序而變化)。
var i1 = 5
fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)
這個(gè)地址可以存儲(chǔ)在一個(gè)叫做指針的特殊數(shù)據(jù)類型中,在本例中這是一個(gè)指向 int 的指針,即 i1:此處使用 *int 表示。如果我們想調(diào)用指針 intP,我們可以這樣聲明它:
var intP *int
然后使用 intP = &i1 是合法的,此時(shí) intP 指向 i1。
(指針的格式化標(biāo)識(shí)符為 %p)
intP 存儲(chǔ)了 i1 的內(nèi)存地址;它指向了 i1 的位置,它引用了變量 i1。
一個(gè)指針變量可以指向任何一個(gè)值的內(nèi)存地址 它指向那個(gè)值的內(nèi)存地址,在 32 位機(jī)器上占用 4 個(gè)字節(jié),在 64 位機(jī)器上占用 8 個(gè)字節(jié),并且與它所指向的值的大小無關(guān)。當(dāng)然,可以聲明指針指向任何類型的值來表明它的原始性或結(jié)構(gòu)性;你可以在指針類型前面加上 號(hào)(前綴)來獲取指針?biāo)赶虻膬?nèi)容,這里的 號(hào)是一個(gè)類型更改器。使用一個(gè)指針引用一個(gè)值被稱為間接引用。
當(dāng)一個(gè)指針被定義后沒有分配到任何變量時(shí),它的值為 nil。
一個(gè)指針變量通常縮寫為 ptr。
注意事項(xiàng)
在書寫表達(dá)式類似 var p *type 時(shí),切記在 號(hào)和指針名稱間留有一個(gè)空格,因?yàn)?`- var ptype` 是語法正確的,但是在更復(fù)雜的表達(dá)式中,它容易被誤認(rèn)為是一個(gè)乘法表達(dá)式!
符號(hào) 可以放在一個(gè)指針前,如 `intP`,那么它將得到這個(gè)指針指向地址上所存儲(chǔ)的值;這被稱為反引用(或者內(nèi)容或者間接引用)操作符;另一種說法是指針轉(zhuǎn)移。
對(duì)于任何一個(gè)變量 var, 如下表達(dá)式都是正確的:var == *(&var)。
現(xiàn)在,我們應(yīng)當(dāng)能理解 pointer.go 的全部?jī)?nèi)容及其輸出:
示例 4.21 pointer.go:
package main
import "fmt"
func main() {
var i1 = 5
fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1)
var intP *int
intP = &i1
fmt.Printf("The value at memory location %p is %d\n", intP, *intP)
}
輸出:
An integer: 5, its location in memory: 0x24f0820
The value at memory location 0x24f0820 is 5
我們可以用下圖來表示內(nèi)存使用的情況:
http://wiki.jikexueyuan.com/project/the-way-to-go/images/4.4.9_fig4.4.png?raw=true" alt="" />
程序 string_pointer.go 為我們展示了指針對(duì)string的例子。
它展示了分配一個(gè)新的值給 *p 并且更改這個(gè)變量自己的值(這里是一個(gè)字符串)。
示例 4.22 string_pointer.go
package main
import "fmt"
func main() {
s := "good bye"
var p *string = &s
*p = "ciao"
fmt.Printf("Here is the pointer p: %p\n", p) // prints address
fmt.Printf("Here is the string *p: %s\n", *p) // prints string
fmt.Printf("Here is the string s: %s\n", s) // prints same string
}
輸出:
Here is the pointer p: 0x2540820
Here is the string *p: ciao
Here is the string s: ciao
通過對(duì) *p 賦另一個(gè)值來更改“對(duì)象”,這樣 s 也會(huì)隨之更改。
內(nèi)存示意圖如下:
http://wiki.jikexueyuan.com/project/the-way-to-go/images/4.4.9_fig4.5.png?raw=true" alt="" />
注意事項(xiàng)
你不能得到一個(gè)文字或常量的地址,例如:
const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10
所以說,Go 語言和 C、C++ 以及 D 語言這些低級(jí)(系統(tǒng))語言一樣,都有指針的概念。但是對(duì)于經(jīng)常導(dǎo)致 C 語言內(nèi)存泄漏繼而程序崩潰的指針運(yùn)算(所謂的指針?biāo)惴?,如?code>pointer+2,移動(dòng)指針指向字符串的字節(jié)數(shù)或數(shù)組的某個(gè)位置)是不被允許的。Go 語言中的指針保證了內(nèi)存安全,更像是 Java、C# 和 VB.NET 中的引用。
因此 c = *p++ 在 Go 語言的代碼中是不合法的。
指針的一個(gè)高級(jí)應(yīng)用是你可以傳遞一個(gè)變量的引用(如函數(shù)的參數(shù)),這樣不會(huì)傳遞變量的拷貝。指針傳遞是很廉價(jià)的,只占用 4 個(gè)或 8 個(gè)字節(jié)。當(dāng)程序在工作中需要占用大量的內(nèi)存,或很多變量,或者兩者都有,使用指針會(huì)減少內(nèi)存占用和提高效率。被指向的變量也保存在內(nèi)存中,直到?jīng)]有任何指針指向它們,所以從它們被創(chuàng)建開始就具有相互獨(dú)立的生命周期。
另一方面(雖然不太可能),由于一個(gè)指針導(dǎo)致的間接引用(一個(gè)進(jìn)程執(zhí)行了另一個(gè)地址),指針的過度頻繁使用也會(huì)導(dǎo)致性能下降。
指針也可以指向另一個(gè)指針,并且可以進(jìn)行任意深度的嵌套,導(dǎo)致你可以有多級(jí)的間接引用,但在大多數(shù)情況這會(huì)使你的代碼結(jié)構(gòu)不清晰。
如我們所見,在大多數(shù)情況下 Go 語言可以使程序員輕松創(chuàng)建指針,并且隱藏間接引用,如:自動(dòng)反向引用。
對(duì)一個(gè)空指針的反向引用是不合法的,并且會(huì)使程序崩潰:
示例 4.23 testcrash.go:
package main
func main() {
var p *int = nil
*p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference
問題 4.2 列舉 Go 語言中 * 號(hào)的所有用法。