此篇文章后續(xù)的若干文章將介紹Go中更多的類(lèi)型。為了更容易和更深刻地理解那些類(lèi)型,最好先閱讀一下本文。
Go可以被看作是一門(mén)C語(yǔ)言血統(tǒng)的語(yǔ)言,這可以通過(guò)此前的指針和結(jié)構(gòu)體兩篇文章得以驗(yàn)證。 Go中的指針和結(jié)構(gòu)體類(lèi)型的內(nèi)存結(jié)構(gòu)和C語(yǔ)言很類(lèi)似。
另一方面,Go也可以被看作是C語(yǔ)言的一個(gè)擴(kuò)展框架。 在C中,值的內(nèi)存結(jié)構(gòu)都是很透明的;但在Go中,對(duì)于某些類(lèi)型的值,其內(nèi)存結(jié)構(gòu)卻不是很透明。 在C中,每個(gè)值在內(nèi)存中只占據(jù)一個(gè)內(nèi)存塊(一段連續(xù)內(nèi)存);但是,一些Go類(lèi)型的值可能占據(jù)多個(gè)內(nèi)存塊。
以后,我們稱(chēng)一個(gè)Go值分布在不同內(nèi)存塊上的部分為此值的各個(gè)值部(value part)。 一個(gè)分布在多個(gè)內(nèi)存塊上的值含有一個(gè)直接值部和若干被此直接值部引用著的間接值部。
上面的段落描述了兩個(gè)類(lèi)別的Go類(lèi)型。下表將列出這兩個(gè)類(lèi)別(category)中的類(lèi)型(type)種類(lèi)(kind):
每個(gè)值在內(nèi)存中只分布在一個(gè)內(nèi)存塊上的類(lèi)型 | 每個(gè)值在內(nèi)存中會(huì)分布在多個(gè)內(nèi)存塊上的類(lèi)型 |
---|---|
![]() |
![]() |
布爾類(lèi)型
各種數(shù)值類(lèi)型 指針類(lèi)型 非類(lèi)型安全指針類(lèi)型 結(jié)構(gòu)體類(lèi)型 數(shù)組類(lèi)型 |
切片類(lèi)型
映射類(lèi)型 通道類(lèi)型 函數(shù)類(lèi)型 接口類(lèi)型 字符串類(lèi)型 |
表中列出的很多類(lèi)型將在后續(xù)文章中逐一詳細(xì)講解。本文的目的就是為了給后續(xù)的講解做一個(gè)鋪墊。
注意:
通過(guò)封裝了很多具體的實(shí)現(xiàn)細(xì)節(jié),第二個(gè)類(lèi)別中的類(lèi)型給Go編程帶來(lái)了很大的便利。 不同的編譯器實(shí)現(xiàn)會(huì)采用不同的內(nèi)部結(jié)構(gòu)來(lái)實(shí)現(xiàn)這些類(lèi)型,但是這些類(lèi)型的值的外在表現(xiàn)必須滿(mǎn)足Go白皮書(shū)中的要求。 此分類(lèi)中的類(lèi)型對(duì)于編程來(lái)說(shuō)并非是很基礎(chǔ)的類(lèi)型。 我們可以使用第一個(gè)分類(lèi)中的類(lèi)型來(lái)實(shí)現(xiàn)此分類(lèi)中的類(lèi)型。 但是,通過(guò)將一些常用或者很獨(dú)特的功能封裝到此第二個(gè)分類(lèi)中的類(lèi)型里,使用Go編程的效率將得到大大提升,體驗(yàn)將得到大大增強(qiáng)。
另一方面,這些封裝同時(shí)也隱藏了這些類(lèi)型的值的內(nèi)部結(jié)構(gòu),使得Go程序員不能對(duì)這些類(lèi)型有一個(gè)更全局更深刻的認(rèn)識(shí)。有時(shí)候這會(huì)對(duì)更好地理解Go帶來(lái)了一些障礙。
為了幫助Go程序員更好的理解第二個(gè)分類(lèi)中的類(lèi)型和它們的值,本文余下的內(nèi)容將對(duì)這些類(lèi)型的內(nèi)在實(shí)現(xiàn)做一個(gè)簡(jiǎn)單介紹。 這些實(shí)現(xiàn)的細(xì)節(jié)將不會(huì)在本文中談及。本文的介紹主要基于(但并不完全符合)官方標(biāo)準(zhǔn)編譯器的實(shí)現(xiàn)。
在繼續(xù)下面的內(nèi)容之前,我們先了解一下Go中的兩種指針類(lèi)型并明確一下“引用”這個(gè)詞的含義。
我們已經(jīng)在上上篇文章中了解了Go中的指針。 那篇文章中所介紹的指針屬于類(lèi)型安全的指針。事實(shí)上,Go還支持另一種稱(chēng)為非類(lèi)型安全的指針類(lèi)型。 非類(lèi)型安全的指針類(lèi)型提供在unsafe
標(biāo)準(zhǔn)庫(kù)包中。
非類(lèi)型安全指針類(lèi)型通常使用unsafe.Pointer
來(lái)表示。 unsafe.Pointer
類(lèi)似于C語(yǔ)言中的void*
。
在《Go語(yǔ)言101》中的大多數(shù)文章中,如果沒(méi)有特別說(shuō)明,當(dāng)一個(gè)指針類(lèi)型被談及,它表示一個(gè)類(lèi)型安全指針。 但是在本文的余下內(nèi)容中,當(dāng)一個(gè)指針被談及,它可能表示一個(gè)類(lèi)型安全指針,也可能表示一個(gè)非類(lèi)型安全指針。
一個(gè)指針值存儲(chǔ)著另一個(gè)值的地址,除非此指針值是一個(gè)nil空指針。 我們可以說(shuō)此指針引用著另外一個(gè)值,或者說(shuō)另外一個(gè)值正被此指針?biāo)谩?一個(gè)值可能被間接引用,比如
a
含有一個(gè)指針字段b
并且這個(gè)指針字段b
引用著另外一個(gè)值c
,那么我們可以說(shuō)結(jié)構(gòu)體值a
也引用著值c
。x
(直接或者間接地)引用著另一個(gè)值y
,并且值y
(直接或者間接地)引用著第三個(gè)值z
,則我們可以說(shuō)值x
間接地引用著值z
。以后,我們將一個(gè)含有(直接或者間接)指針字段的結(jié)構(gòu)體類(lèi)型稱(chēng)為一個(gè)指針包裹類(lèi)型,將一個(gè)含有(直接或者間接)指針的類(lèi)型稱(chēng)為指針持有者類(lèi)型。 指針類(lèi)型和指針包裹類(lèi)型都屬于指針持有者類(lèi)型。元素類(lèi)型為指針持有者類(lèi)型的數(shù)組類(lèi)型也是指針持有者類(lèi)型(數(shù)組將在下一篇文章中介紹)。
為了更好地理解第二個(gè)分類(lèi)中的類(lèi)型的值的運(yùn)行時(shí)刻行為,我們可以認(rèn)為這些類(lèi)型在內(nèi)部是使用第一個(gè)分類(lèi)中的類(lèi)型來(lái)定義的(如下所示)。 如果你以前并沒(méi)有很多使用過(guò)Go中各種類(lèi)型的經(jīng)驗(yàn),目前你不必深刻地理解這些定義。 對(duì)這些定義擁有一個(gè)粗糙的印象足夠?qū)斫夂罄m(xù)文章中將要講解的類(lèi)型有所幫助。 你可以在今后有了更多的Go編程經(jīng)驗(yàn)之后再重讀一下本文。
映射、通道和函數(shù)類(lèi)型的內(nèi)部定義很相似:
// 映射類(lèi)型
type _map *hashtableImpl // 目前,官方標(biāo)準(zhǔn)編譯器是使用
// 哈希表來(lái)實(shí)現(xiàn)映射的。
// 通道類(lèi)型
type _channel *channelImpl
// 函數(shù)類(lèi)型
type _function *functionImpl
從這些定義,我們可以看出來(lái),這三個(gè)種類(lèi)的類(lèi)型的內(nèi)部結(jié)構(gòu)其實(shí)是一個(gè)指針類(lèi)型。 或者說(shuō),這些類(lèi)型的值的直接部分在內(nèi)部是一個(gè)指針。 這些類(lèi)型的每個(gè)值的直接部分引用著它的具體實(shí)現(xiàn)的底層間接部分。
切片類(lèi)型的內(nèi)部定義:
type _slice struct {
elements unsafe.Pointer // 引用著底層的元素
len int // 當(dāng)前的元素個(gè)數(shù)
cap int // 切片的容量
}
從這個(gè)定義可以看出來(lái),一個(gè)切片類(lèi)型在內(nèi)部可以看作是一個(gè)指針包裹類(lèi)型。 每個(gè)非零切片值包含著一個(gè)底層間接部分用來(lái)存儲(chǔ)此切片的元素。 一個(gè)切片值的底層元素序列(間接部分)被此切片值的elements
字段所引用。
type _string struct {
elements *byte // 引用著底層的byte元素
len int // 字符串的長(zhǎng)度
}
從此定義可以看出,每個(gè)字符串類(lèi)型在內(nèi)部也可以看作是一個(gè)指針包裹類(lèi)型。 每個(gè)非零字符串值含有一個(gè)指針字段 elements
。 這個(gè)指針字段引用著此字符串值的底層字節(jié)元素序列。
我們可以認(rèn)為接口類(lèi)型在內(nèi)部是如下定義的:
type _interface struct {
dynamicType *_type // 引用著接口值的動(dòng)態(tài)類(lèi)型
dynamicValue unsafe.Pointer // 引用著接口值的動(dòng)態(tài)值
}
從這個(gè)定義來(lái)看,接口類(lèi)型也可以看作是一個(gè)指針包裹類(lèi)型。一個(gè)接口類(lèi)型含有兩個(gè)指針字段。 每個(gè)非零接口值的(兩個(gè))間接部分分別存儲(chǔ)著此接口值的動(dòng)態(tài)類(lèi)型和動(dòng)態(tài)值。 這兩個(gè)間接部分被此接口值的直接字段dynamicType
和dynamicValue
所引用。
事實(shí)上,上面這個(gè)內(nèi)部定義只用于表示空接口類(lèi)型的值??战涌陬?lèi)型沒(méi)有指定任何方法。 后面的接口一文詳細(xì)解釋了接口類(lèi)型和值。 非空接口類(lèi)型的內(nèi)部定義如下:
type _interface struct {
dynamicTypeInfo *struct {
dynamicType *_type // 引用著接口值的動(dòng)態(tài)類(lèi)型
methods []*_function // 引用著動(dòng)態(tài)類(lèi)型的對(duì)應(yīng)方法列表
}
dynamicValue unsafe.Pointer // 引用著動(dòng)態(tài)值
}
一個(gè)非空接口類(lèi)型的值的dynamicTypeInfo
字段的methods
字段引用著一個(gè)方法列表。 此列表中的每一項(xiàng)為此接口值的動(dòng)態(tài)類(lèi)型上定義的一個(gè)方法,此方法對(duì)應(yīng)著此接口類(lèi)型所指定的一個(gè)的同描述的方法。
現(xiàn)在我們了解了第二個(gè)分類(lèi)中的類(lèi)型的內(nèi)部結(jié)構(gòu)是一個(gè)指針持有(指針或者指針包裹)類(lèi)型。 這對(duì)于我們理解Go中的值復(fù)制行為有很大幫助。
在Go中,每個(gè)賦值操作(包括函數(shù)調(diào)用傳參等)都是一個(gè)值的淺復(fù)制過(guò)程(假設(shè)源值和目標(biāo)值的類(lèi)型相同)。 換句話說(shuō),在一個(gè)賦值操作中,只有源值的直接部分被復(fù)制給了目標(biāo)值。 如果源值含有間接部分,則在此賦值操作完成之后,目標(biāo)值和源值的直接部分將引用著相同的間接部分。 換句話說(shuō),兩個(gè)值將共享底層的間接值部,如下圖所示:
事實(shí)上,對(duì)于字符串值和接口值的賦值,上述描述在理論上并非百分百正確。 官方FAQ明確說(shuō)明了在一個(gè)接口值的賦值中,接口的底層動(dòng)態(tài)值將被復(fù)制到目標(biāo)值。 但是,因?yàn)橐粋€(gè)接口值的動(dòng)態(tài)值是只讀的,所以在接口值的賦值中,官方標(biāo)準(zhǔn)編譯器并沒(méi)有復(fù)制底層的動(dòng)態(tài)值。這可以被視為是一個(gè)編譯器優(yōu)化。 對(duì)于字符串值的賦值,道理是一樣的。所以對(duì)于官方標(biāo)準(zhǔn)編譯器來(lái)說(shuō),上一段的描述是100%正確的。
因?yàn)橐粋€(gè)間接值部可能并不專(zhuān)屬于任何一個(gè)值,所以在使用unsafe.Sizeof
函數(shù)計(jì)算一個(gè)值的尺寸的時(shí)候,此值的間接部分所占內(nèi)存空間未被計(jì)算在內(nèi)。
“引用”這個(gè)術(shù)語(yǔ)在Go社區(qū)中使用得有些混亂。很多Go程序員在Go編程中可能由此產(chǎn)生了一些困惑。 一些文檔或者網(wǎng)絡(luò)文章,包括一些官方文檔,把“引用”(reference)看作是“值”(value)的一個(gè)對(duì)立面。 《Go語(yǔ)言101》強(qiáng)烈不推薦這種定義。在這一點(diǎn)上,本人不想爭(zhēng)論什么。這里僅僅列出一些肯定錯(cuò)誤地使用了“引用”這個(gè)術(shù)語(yǔ)的例子:
我并不是想說(shuō)引用類(lèi)型這個(gè)術(shù)語(yǔ)在Go中是完全沒(méi)有價(jià)值的, 我只是想表達(dá)這個(gè)術(shù)語(yǔ)是完全沒(méi)有必要的,并且它常常在Go的使用中導(dǎo)致一些困惑。我推薦使用指針持有者類(lèi)型來(lái)代替這個(gè)術(shù)語(yǔ)。 另外,我個(gè)人的觀點(diǎn)是最好將引用這個(gè)詞限定到只表示值之間的關(guān)系,把它當(dāng)作一個(gè)動(dòng)詞或者名詞來(lái)使用,永遠(yuǎn)不要把它當(dāng)作一個(gè)形容詞來(lái)使用。 這樣將在使用Go的過(guò)程中避免很多困惑。
更多建議: