雖然Go吸收融合了很多其語(yǔ)言中的各種特性,但是Go主要被歸入C語(yǔ)言家族。其中一個(gè)重要的原因就是Go和C一樣,也支持指針。 當(dāng)然Go中的指針相比C指針有很多限制。本篇文章將介紹指針相關(guān)的各種概念和Go指針相關(guān)的各種細(xì)節(jié)。
在編程中,一個(gè)內(nèi)存地址用來(lái)定位一段內(nèi)存。
通常地,一個(gè)內(nèi)存地址用一個(gè)操作系統(tǒng)原生字(native word)來(lái)存儲(chǔ)。 一個(gè)原生字在32位操作系統(tǒng)上占4個(gè)字節(jié),在64位操作系統(tǒng)上占8個(gè)字節(jié)。 所以,32位操作系統(tǒng)上的理論最大支持內(nèi)存容量為4GB(1GB == 230字節(jié)),64位操作系統(tǒng)上的理論最大支持內(nèi)存容量為264Byte,即16EB(EB:艾字節(jié),1EB == 1024PB, 1PB == 1024TB, 1TB == 1024GB)。
內(nèi)存地址的字面形式常用整數(shù)的十六進(jìn)制字面量來(lái)表示,比如?0x1234CDEF
?。
以后我們常簡(jiǎn)稱(chēng)內(nèi)存地址為地址。
一個(gè)值的地址是指此值的直接部分占據(jù)的內(nèi)存的起始地址。在Go中,每個(gè)值都包含一個(gè)直接部分,但有些值可能還包含一個(gè)或多個(gè)間接部分,下下章將對(duì)此詳述。
指針是Go中的一種類(lèi)型分類(lèi)(kind)。 一個(gè)指針可以存儲(chǔ)一個(gè)內(nèi)存地址;從地址通常為另外一個(gè)值的地址。
和C指針不一樣,為了安全起見(jiàn),Go指針有很多限制,詳見(jiàn)下面的章節(jié)。
在Go中,一個(gè)無(wú)名指針類(lèi)型的字面形式為?*T
?,其中?T
?為一個(gè)任意類(lèi)型。類(lèi)型?T
?稱(chēng)為指針類(lèi)型?*T
?的基類(lèi)型(base type)。 如果一個(gè)指針類(lèi)型的基類(lèi)型為?T
?,則我們可以稱(chēng)此指針類(lèi)型為一個(gè)?T
?指針類(lèi)型。
雖然我們可以聲明具名指針類(lèi)型,但是一般不推薦這么做,因?yàn)闊o(wú)名指針類(lèi)型的可讀性更高。
如果一個(gè)指針類(lèi)型的底層類(lèi)型是?*T
?,則它的基類(lèi)型為?T
?。
如果兩個(gè)無(wú)名指針類(lèi)型的基類(lèi)型為同一類(lèi)型,則這兩個(gè)無(wú)名指針類(lèi)型亦為同一類(lèi)型。
一些指針類(lèi)型的例子:
*int // 一個(gè)基類(lèi)型為int的無(wú)名指針類(lèi)型。
**int // 一個(gè)多級(jí)無(wú)名指針類(lèi)型,它的基類(lèi)型為*int。
type Ptr *int // Ptr是一個(gè)具名指針類(lèi)型,它的基類(lèi)型為int。
type PP *Ptr // PP是一個(gè)具名多級(jí)指針類(lèi)型,它的基類(lèi)型為Ptr。
指針類(lèi)型的零值的字面量使用預(yù)聲明的?nil
?來(lái)表示。一個(gè)nil指針(常稱(chēng)為空指針)中不存儲(chǔ)任何地址。
如果一個(gè)指針類(lèi)型的基類(lèi)型為?T
?,則此指針類(lèi)型的值只能存儲(chǔ)類(lèi)型為?T
?的值的地址。
在《Go語(yǔ)言101》中,術(shù)語(yǔ)“引用”暗示著一個(gè)關(guān)系。比如,如果一個(gè)指針中存儲(chǔ)著另外一個(gè)值的地址,則我們可以說(shuō)此指針值引用著另外一個(gè)值;同時(shí)另外一個(gè)值當(dāng)前至少有一個(gè)引用。 本書(shū)對(duì)此術(shù)語(yǔ)的使用和Go白皮書(shū)是一致的。
當(dāng)一個(gè)指針引用著另外一個(gè)值,我們也常說(shuō)此指針指向另外一個(gè)值。
有兩種方式來(lái)得到一個(gè)指針值:
new
?來(lái)為任何類(lèi)型的值開(kāi)辟一塊內(nèi)存并將此內(nèi)存塊的起始地址做為此值的地址返回。 假設(shè)?T
?是任一類(lèi)型,則函數(shù)調(diào)用?new(T)
?返回一個(gè)類(lèi)型為?*T
?的指針值。 存儲(chǔ)在返回指針值所表示的地址處的值(可被看作是一個(gè)匿名變量)為?T
?的零值。&
?來(lái)獲取一個(gè)可尋址的值的地址。 對(duì)于一個(gè)類(lèi)型為?T
?的可尋址的值?t
?,我們可以用?&t
?來(lái)取得它的地址。?&t
?的類(lèi)型為?*T
?。一般說(shuō)來(lái),一個(gè)可尋址的值是指被放置在內(nèi)存中某固定位置處的一個(gè)值(但放置在某固定位置處的一個(gè)值并非一定是可尋址的)。 目前,我們只需知道所有變量都是可以尋址的;但是所有常量、函數(shù)返回值和強(qiáng)制轉(zhuǎn)換結(jié)果都是不可尋址的。 當(dāng)一個(gè)變量被聲明的時(shí)候,Go運(yùn)行時(shí)將為此變量開(kāi)辟一段內(nèi)存。此內(nèi)存的起始地址即為此變量的地址。
更多可被(或不可被)尋址的值將在以后的文章中逐漸提及。 如果你已經(jīng)對(duì)Go比較熟悉,你可以閱讀此條總結(jié)來(lái)了解在Go中哪些值可以或不可以被尋址。
下一節(jié)中的例子將展示如何獲取一些值的地址。
我們可以使用前置解引用操作符?*
?來(lái)訪問(wèn)存儲(chǔ)在一個(gè)指針?biāo)硎镜牡刂诽幍闹担创酥羔標(biāo)弥闹担?比如,對(duì)于基類(lèi)型為?T
?的指針類(lèi)型的一個(gè)指針值?p
?,我們可以用?*p
?來(lái)表示地址?p
?處的值。 此值的類(lèi)型為?T
?。?*p
?稱(chēng)為指針?p
?的解引用。解引用是取地址的逆過(guò)程。
解引用一個(gè)nil指針將產(chǎn)生一個(gè)恐慌。
下面這個(gè)例子展示了如何取地址和解引用。
package main
import "fmt"
func main() {
p0 := new(int) // p0指向一個(gè)int類(lèi)型的零值
fmt.Println(p0) // (打印出一個(gè)十六進(jìn)制形式的地址)
fmt.Println(*p0) // 0
x := *p0 // x是p0所引用的值的一個(gè)復(fù)制。
p1, p2 := &x, &x // p1和p2中都存儲(chǔ)著x的地址。
// x、*p1和*p2表示著同一個(gè)int值。
fmt.Println(p1 == p2) // true
fmt.Println(p0 == p1) // false
p3 := &*p0 // <=> p3 := &(*p0)
// <=> p3 := p0
// p3和p0中存儲(chǔ)的地址是一樣的。
fmt.Println(p0 == p3) // true
*p0, *p1 = 123, 789
fmt.Println(*p2, x, *p3) // 789 789 123
fmt.Printf("%T, %T \n", *p0, x) // int, int
fmt.Printf("%T, %T \n", p0, p1) // *int, *int
}
下面這張圖描繪了上面這個(gè)例子中各個(gè)值之間的關(guān)系。
讓我們先看一個(gè)例子:
package main
import "fmt"
func double(x int) {
x += x
}
func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}
我們本期望上例中的?double
?函數(shù)將變量?a
?的值放大為原來(lái)的兩倍,但是事實(shí)證明我們的期望沒(méi)有得到實(shí)現(xiàn)。 為什么呢?因?yàn)樵贕o中,所有的賦值(包括函數(shù)調(diào)用傳參)過(guò)程都是一個(gè)值復(fù)制過(guò)程。 所以在上面的?double
?函數(shù)體內(nèi)修改的是變量?a
?的一個(gè)副本,而沒(méi)有修改變量?a
?本身。
當(dāng)然我們可以讓double函數(shù)返回輸入?yún)?shù)的兩倍數(shù),但是此方法并非適用于所有場(chǎng)合。 下面這個(gè)例子通過(guò)將輸入?yún)?shù)的類(lèi)型改為一個(gè)指針類(lèi)型來(lái)達(dá)到同樣的目的。
package main
import "fmt"
func double(x *int) {
*x += *x
x = nil // 此行僅為講解目的
}
func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}
從上例可以看出,通過(guò)將?double
?函數(shù)的輸入?yún)?shù)的類(lèi)型改為?*int
?,傳入的實(shí)參?&a
?和它在此函數(shù)體內(nèi)的一個(gè)副本?x
?都引用著變量?a
?。 所以對(duì)?*x
?的修改等價(jià)于對(duì)?*p
?(也就是變量?a
?)的修改。 換句話說(shuō),新版本的?double
?函數(shù)內(nèi)的操作可以反映到此函數(shù)外了。
當(dāng)然,在此函數(shù)體內(nèi)對(duì)傳入的指針實(shí)參的修改?x = nil
?依舊不能反映到函數(shù)外,因?yàn)榇诵薷陌l(fā)生在此指針的一個(gè)副本上。 所以在?double
?函數(shù)調(diào)用之后,局部變量?p
?的值并沒(méi)有被修改為?nil
?。
簡(jiǎn)而言之,指針提供了一種間接的途徑來(lái)訪問(wèn)和修改一些值。 雖然很多語(yǔ)言中沒(méi)有指針這個(gè)概念,但是指針被隱藏其它概念之中。
和C不一樣,Go是支持垃圾回收的,所以一個(gè)函數(shù)返回其內(nèi)聲明的局部變量的地址是絕對(duì)安全的。比如:
func newInt() *int {
a := 3
return &a
}
為了安全起見(jiàn),Go指針在使用上相對(duì)于C指針有很多限制。 通過(guò)施加這些限制,Go指針保留了C指針的好處,同時(shí)也避免了C指針的危險(xiǎn)性。
在Go中,指針是不能參與算術(shù)運(yùn)算的。比如,對(duì)于一個(gè)指針?p
?, 運(yùn)算?p++
?和?p-2
?都是非法的。
如果?p
?為一個(gè)指向一個(gè)數(shù)值類(lèi)型值的指針,?*p++
?將被編譯器認(rèn)為是合法的并且等價(jià)于?(*p)++
?。 換句話說(shuō),解引用操作符?*
?的優(yōu)先級(jí)都高于自增?++
?和自減?--
?操作符。
例子:
package main
import "fmt"
func main() {
a := int64(5)
p := &a
// 下面這兩行編譯不通過(guò)。
/*
p++
p = (&a) + 8
*/
*p++
fmt.Println(*p, a) // 6 6
fmt.Println(p == &a) // true
*&a++
*&*&a++
**&p++
*&*p++
fmt.Println(*p, a) // 10 10
}
在Go中,只有如下某個(gè)條件被滿足的情況下,一個(gè)類(lèi)型為T1的指針值才能被顯式轉(zhuǎn)換為另一個(gè)指針類(lèi)型T2:
T1
?和?T2
?的底層類(lèi)型必須一致(忽略結(jié)構(gòu)體字段的標(biāo)簽)。 特別地,如果類(lèi)型?T1
?和?T2
?中只要有一個(gè)是無(wú)名類(lèi)型并且它們的底層類(lèi)型一致(考慮結(jié)構(gòu)體字段的標(biāo)簽),則此轉(zhuǎn)換可以是隱式的。 關(guān)于結(jié)構(gòu)體,請(qǐng)參閱下一篇文章。T1
?和?T2
?都為無(wú)名類(lèi)型并且它們的基類(lèi)型的底層類(lèi)型一致(忽略結(jié)構(gòu)體字段的標(biāo)簽)。比如,
type MyInt int64
type Ta *int64
type Tb *MyInt
對(duì)于上面所示的這些指針類(lèi)型,下面的事實(shí)成立:
*int64
?的值可以被隱式轉(zhuǎn)換到類(lèi)型?Ta
?,反之亦然(因?yàn)樗鼈兊牡讓宇?lèi)型均為?*int64
?)。*MyInt
?的值可以被隱式轉(zhuǎn)換到類(lèi)型?Tb
?,反之亦然(因?yàn)樗鼈兊牡讓宇?lèi)型均為?*MyInt
?)。*MyInt
?的值可以被顯式轉(zhuǎn)換為類(lèi)型?*int64
?,反之亦然(因?yàn)樗鼈兌际菬o(wú)名的并且它們的基類(lèi)型的底層類(lèi)型均為?int64
?)。Ta
?的值不能直接被轉(zhuǎn)換為類(lèi)型?Tb
?,即使是顯式轉(zhuǎn)換也是不行的。 但是,通過(guò)上述三條事實(shí),通過(guò)三層顯式轉(zhuǎn)換?Tb((*MyInt)((*int64)(ta)))
?,一個(gè)類(lèi)型為?Ta
?的值?ta
?可以被間接地轉(zhuǎn)換為類(lèi)型?Tb
?。這些指針類(lèi)型的任何值都無(wú)法被轉(zhuǎn)換到類(lèi)型?*uint64
?。
Go指針值是支持(使用比較運(yùn)算符==和!=)比較的。 但是,兩個(gè)指針只有在下列任一條件被滿足的時(shí)候才可以比較:
nil
?標(biāo)識(shí)符表示。例子:
package main
func main() {
type MyInt int64
type Ta *int64
type Tb *MyInt
// 4個(gè)不同類(lèi)型的指針:
var pa0 Ta
var pa1 *int64
var pb0 Tb
var pb1 *MyInt
// 下面這6行編譯沒(méi)問(wèn)題。它們的比較結(jié)果都為true。
_ = pa0 == pa1
_ = pb0 == pb1
_ = pa0 == nil
_ = pa1 == nil
_ = pb0 == nil
_ = pb1 == nil
// 下面這三行編譯不通過(guò)。
/*
_ = pa0 == pb0
_ = pa1 == pb1
_ = pa0 == Tb(nil)
*/
}
一個(gè)指針值可以被賦值給另一個(gè)指針值的條件和這兩個(gè)指針值可以比較的條件(見(jiàn)上一小節(jié))是一致的。
unsafe標(biāo)準(zhǔn)庫(kù)包中提供的非類(lèi)型安全指針(?unsafe.Pointer
?)機(jī)制可以被用來(lái)打破上述Go指針的安全限制。 ?unsafe.Pointer
?類(lèi)型類(lèi)似于C語(yǔ)言中的?void*
?。 但是,通常地,非類(lèi)型安全指針機(jī)制不推薦在Go日常編程中使用。
更多建議: