Go語言 值部 - 為了更容易和更深刻地理解Go中地各種值

2023-02-16 17:37 更新

此篇文章后續(xù)的若干文章將介紹Go中更多的類型。為了更容易和更深刻地理解那些類型,最好先閱讀一下本文。

Go類型分為兩大類別(category)

Go可以被看作是一門C語言血統(tǒng)的語言,這可以通過此前的指針結(jié)構(gòu)體兩篇文章得以驗(yàn)證。 Go中的指針和結(jié)構(gòu)體類型的內(nèi)存結(jié)構(gòu)和C語言很類似。

另一方面,Go也可以被看作是C語言的一個(gè)擴(kuò)展框架。 在C中,值的內(nèi)存結(jié)構(gòu)都是很透明的;但在Go中,對(duì)于某些類型的值,其內(nèi)存結(jié)構(gòu)卻不是很透明。 在C中,每個(gè)值在內(nèi)存中只占據(jù)一個(gè)內(nèi)存塊(一段連續(xù)內(nèi)存);但是,一些Go類型的值可能占據(jù)多個(gè)內(nèi)存塊。

以后,我們稱一個(gè)Go值分布在不同內(nèi)存塊上的部分為此值的各個(gè)值部(value part)。 一個(gè)分布在多個(gè)內(nèi)存塊上的值含有一個(gè)直接值部和若干被此直接值部引用著的間接值部。

上面的段落描述了兩個(gè)類別的Go類型。下表將列出這兩個(gè)類別(category)中的類型(type)種類(kind):

每個(gè)值在內(nèi)存中只分布在一個(gè)內(nèi)存塊上的類型 每個(gè)值在內(nèi)存中會(huì)分布在多個(gè)內(nèi)存塊上的類型
單值部 多值部
布爾類型
各種數(shù)值類型
指針類型
非類型安全指針類型
結(jié)構(gòu)體類型
數(shù)組類型
切片類型
映射類型
通道類型
函數(shù)類型
接口類型
字符串類型

表中列出的很多類型將在后續(xù)文章中逐一詳細(xì)講解。本文的目的就是為了給后續(xù)的講解做一個(gè)鋪墊。

注意:

  • 接口類型和字符串類型值是否包含間接部分取決于具體編譯器實(shí)現(xiàn)。 如果不使用今后將介紹的非類型安全途徑,我們無法從這兩類類型的值的外在表現(xiàn)來判定它們的值是否含有間接部分。 在《Go語言101》中,我們認(rèn)為這兩類類型的值是可能包含間接值部的。
  • 同樣地,函數(shù)類型的值是否包含間接部分幾乎也是不可能驗(yàn)證的。 在《Go語言101》中,我們認(rèn)為函數(shù)值是可能包含間接值部的。

通過封裝了很多具體的實(shí)現(xiàn)細(xì)節(jié),第二個(gè)類別中的類型給Go編程帶來了很大的便利。 不同的編譯器實(shí)現(xiàn)會(huì)采用不同的內(nèi)部結(jié)構(gòu)來實(shí)現(xiàn)這些類型,但是這些類型的值的外在表現(xiàn)必須滿足Go白皮書中的要求。 此分類中的類型對(duì)于編程來說并非是很基礎(chǔ)的類型。 我們可以使用第一個(gè)分類中的類型來實(shí)現(xiàn)此分類中的類型。 但是,通過將一些常用或者很獨(dú)特的功能封裝到此第二個(gè)分類中的類型里,使用Go編程的效率將得到大大提升,體驗(yàn)將得到大大增強(qiáng)。

另一方面,這些封裝同時(shí)也隱藏了這些類型的值的內(nèi)部結(jié)構(gòu),使得Go程序員不能對(duì)這些類型有一個(gè)更全局更深刻的認(rèn)識(shí)。有時(shí)候這會(huì)對(duì)更好地理解Go帶來了一些障礙。

為了幫助Go程序員更好的理解第二個(gè)分類中的類型和它們的值,本文余下的內(nèi)容將對(duì)這些類型的內(nèi)在實(shí)現(xiàn)做一個(gè)簡單介紹。 這些實(shí)現(xiàn)的細(xì)節(jié)將不會(huì)在本文中談及。本文的介紹主要基于(但并不完全符合)官方標(biāo)準(zhǔn)編譯器的實(shí)現(xiàn)。

Go中的兩種指針類型

在繼續(xù)下面的內(nèi)容之前,我們先了解一下Go中的兩種指針類型并明確一下“引用”這個(gè)詞的含義。

我們已經(jīng)在上上篇文章中了解了Go中的指針。 那篇文章中所介紹的指針屬于類型安全的指針。事實(shí)上,Go還支持另一種稱為非類型安全的指針類型。 非類型安全的指針類型提供在unsafe標(biāo)準(zhǔn)庫包中。 非類型安全指針類型通常使用unsafe.Pointer來表示。 unsafe.Pointer類似于C語言中的void*。

在《Go語言101》中的大多數(shù)文章中,如果沒有特別說明,當(dāng)一個(gè)指針類型被談及,它表示一個(gè)類型安全指針。 但是在本文的余下內(nèi)容中,當(dāng)一個(gè)指針被談及,它可能表示一個(gè)類型安全指針,也可能表示一個(gè)非類型安全指針。

一個(gè)指針值存儲(chǔ)著另一個(gè)值的地址,除非此指針值是一個(gè)nil空指針。 我們可以說此指針引用著另外一個(gè)值,或者說另外一個(gè)值正被此指針?biāo)谩?一個(gè)值可能被間接引用,比如

  • 如果一個(gè)結(jié)構(gòu)體值a含有一個(gè)指針字段b并且這個(gè)指針字段b引用著另外一個(gè)值c,那么我們可以說結(jié)構(gòu)體值a也引用著值c。
  • 如果一個(gè)值x(直接或者間接地)引用著另一個(gè)值y,并且值y(直接或者間接地)引用著第三個(gè)值z,則我們可以說值x間接地引用著值z。

以后,我們將一個(gè)含有(直接或者間接)指針字段的結(jié)構(gòu)體類型稱為一個(gè)指針包裹類型,將一個(gè)含有(直接或者間接)指針的類型稱為指針持有者類型。 指針類型和指針包裹類型都屬于指針持有者類型。元素類型為指針持有者類型的數(shù)組類型也是指針持有者類型(數(shù)組將在下一篇文章中介紹)。

第二個(gè)分類中的類型的(可能的)內(nèi)部實(shí)現(xiàn)結(jié)構(gòu)定義

為了更好地理解第二個(gè)分類中的類型的值的運(yùn)行時(shí)刻行為,我們可以認(rèn)為這些類型在內(nèi)部是使用第一個(gè)分類中的類型來定義的(如下所示)。 如果你以前并沒有很多使用過Go中各種類型的經(jīng)驗(yàn),目前你不必深刻地理解這些定義。 對(duì)這些定義擁有一個(gè)粗糙的印象足夠?qū)斫夂罄m(xù)文章中將要講解的類型有所幫助。 你可以在今后有了更多的Go編程經(jīng)驗(yàn)之后再重讀一下本文。

映射、通道和函數(shù)類型的內(nèi)部定義

映射、通道和函數(shù)類型的內(nèi)部定義很相似:

// 映射類型
type _map *hashtableImpl // 目前,官方標(biāo)準(zhǔn)編譯器是使用
                         // 哈希表來實(shí)現(xiàn)映射的。

// 通道類型
type _channel *channelImpl

// 函數(shù)類型
type _function *functionImpl

從這些定義,我們可以看出來,這三個(gè)種類的類型的內(nèi)部結(jié)構(gòu)其實(shí)是一個(gè)指針類型。 或者說,這些類型的值的直接部分在內(nèi)部是一個(gè)指針。 這些類型的每個(gè)值的直接部分引用著它的具體實(shí)現(xiàn)的底層間接部分。

切片類型的內(nèi)部定義

切片類型的內(nèi)部定義:

type _slice struct {
	elements unsafe.Pointer // 引用著底層的元素
	len      int            // 當(dāng)前的元素個(gè)數(shù)
	cap      int            // 切片的容量
}

從這個(gè)定義可以看出來,一個(gè)切片類型在內(nèi)部可以看作是一個(gè)指針包裹類型。 每個(gè)非零切片值包含著一個(gè)底層間接部分用來存儲(chǔ)此切片的元素。 一個(gè)切片值的底層元素序列(間接部分)被此切片值的elements字段所引用。

字符串類型的內(nèi)部結(jié)構(gòu)

type _string struct {
	elements *byte // 引用著底層的byte元素
	len      int   // 字符串的長度
}

從此定義可以看出,每個(gè)字符串類型在內(nèi)部也可以看作是一個(gè)指針包裹類型。 每個(gè)非零字符串值含有一個(gè)指針字段 elements。 這個(gè)指針字段引用著此字符串值的底層字節(jié)元素序列。

接口類型的內(nèi)部定義

我們可以認(rèn)為接口類型在內(nèi)部是如下定義的:

type _interface struct {
	dynamicType  *_type         // 引用著接口值的動(dòng)態(tài)類型
	dynamicValue unsafe.Pointer // 引用著接口值的動(dòng)態(tài)值
}

從這個(gè)定義來看,接口類型也可以看作是一個(gè)指針包裹類型。一個(gè)接口類型含有兩個(gè)指針字段。 每個(gè)非零接口值的(兩個(gè))間接部分分別存儲(chǔ)著此接口值的動(dòng)態(tài)類型和動(dòng)態(tài)值。 這兩個(gè)間接部分被此接口值的直接字段dynamicTypedynamicValue所引用。

事實(shí)上,上面這個(gè)內(nèi)部定義只用于表示空接口類型的值??战涌陬愋蜎]有指定任何方法。 后面的接口一文詳細(xì)解釋了接口類型和值。 非空接口類型的內(nèi)部定義如下:

type _interface struct {
	dynamicTypeInfo *struct {
		dynamicType *_type       // 引用著接口值的動(dòng)態(tài)類型
		methods     []*_function // 引用著動(dòng)態(tài)類型的對(duì)應(yīng)方法列表
	}
	dynamicValue unsafe.Pointer // 引用著動(dòng)態(tài)值
}

一個(gè)非空接口類型的值的dynamicTypeInfo字段的methods字段引用著一個(gè)方法列表。 此列表中的每一項(xiàng)為此接口值的動(dòng)態(tài)類型上定義的一個(gè)方法,此方法對(duì)應(yīng)著此接口類型所指定的一個(gè)的同描述的方法。

在賦值中,底層間接值部將不會(huì)被復(fù)制

現(xiàn)在我們了解了第二個(gè)分類中的類型的內(nèi)部結(jié)構(gòu)是一個(gè)指針持有(指針或者指針包裹)類型。 這對(duì)于我們理解Go中的值復(fù)制行為有很大幫助。

在Go中,每個(gè)賦值操作(包括函數(shù)調(diào)用傳參等)都是一個(gè)值的淺復(fù)制過程(假設(shè)源值和目標(biāo)值的類型相同)。 換句話說,在一個(gè)賦值操作中,只有源值的直接部分被復(fù)制給了目標(biāo)值。 如果源值含有間接部分,則在此賦值操作完成之后,目標(biāo)值和源值的直接部分將引用著相同的間接部分。 換句話說,兩個(gè)值將共享底層的間接值部,如下圖所示:

值復(fù)制

事實(shí)上,對(duì)于字符串值和接口值的賦值,上述描述在理論上并非百分百正確。 官方FAQ明確說明了在一個(gè)接口值的賦值中,接口的底層動(dòng)態(tài)值將被復(fù)制到目標(biāo)值。 但是,因?yàn)橐粋€(gè)接口值的動(dòng)態(tài)值是只讀的,所以在接口值的賦值中,官方標(biāo)準(zhǔn)編譯器并沒有復(fù)制底層的動(dòng)態(tài)值。這可以被視為是一個(gè)編譯器優(yōu)化。 對(duì)于字符串值的賦值,道理是一樣的。所以對(duì)于官方標(biāo)準(zhǔn)編譯器來說,上一段的描述是100%正確的。

因?yàn)橐粋€(gè)間接值部可能并不專屬于任何一個(gè)值,所以在使用unsafe.Sizeof函數(shù)計(jì)算一個(gè)值的尺寸的時(shí)候,此值的間接部分所占內(nèi)存空間未被計(jì)算在內(nèi)。

關(guān)于術(shù)語“引用類型”和“引用值”

“引用”這個(gè)術(shù)語在Go社區(qū)中使用得有些混亂。很多Go程序員在Go編程中可能由此產(chǎn)生了一些困惑。 一些文檔或者網(wǎng)絡(luò)文章,包括一些官方文檔,把“引用”(reference)看作是“值”(value)的一個(gè)對(duì)立面。 《Go語言101》強(qiáng)烈不推薦這種定義。在這一點(diǎn)上,本人不想爭論什么。這里僅僅列出一些肯定錯(cuò)誤地使用了“引用”這個(gè)術(shù)語的例子:

  • 在Go中,只有切片、映射、通道和函數(shù)類型屬于引用類型。 (如果我們確實(shí)需要引用類型這個(gè)術(shù)語,那么我們不應(yīng)把其它指針持有者類型排除在引用類型之外。)
  • 一些函數(shù)調(diào)用的參數(shù)是通過引用來傳遞的。 (對(duì)不起,在Go中,所有的函數(shù)調(diào)用的參數(shù)都是通過值復(fù)制直接值部的方式來傳遞的。)

我并不是想說引用類型這個(gè)術(shù)語在Go中是完全沒有價(jià)值的, 我只是想表達(dá)這個(gè)術(shù)語是完全沒有必要的,并且它常常在Go的使用中導(dǎo)致一些困惑。我推薦使用指針持有者類型來代替這個(gè)術(shù)語。 另外,我個(gè)人的觀點(diǎn)是最好將引用這個(gè)詞限定到只表示值之間的關(guān)系,把它當(dāng)作一個(gè)動(dòng)詞或者名詞來使用,永遠(yuǎn)不要把它當(dāng)作一個(gè)形容詞來使用。 這樣將在使用Go的過程中避免很多困惑。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)