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

2023-02-16 17:37 更新

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

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

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

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

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

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

每個值在內存中只分布在一個內存塊上的類型 每個值在內存中會分布在多個內存塊上的類型
單值部 多值部
布爾類型
各種數值類型
指針類型
非類型安全指針類型
結構體類型
數組類型
切片類型
映射類型
通道類型
函數類型
接口類型
字符串類型

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

注意:

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

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

另一方面,這些封裝同時也隱藏了這些類型的值的內部結構,使得Go程序員不能對這些類型有一個更全局更深刻的認識。有時候這會對更好地理解Go帶來了一些障礙。

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

Go中的兩種指針類型

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

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

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

一個指針值存儲著另一個值的地址,除非此指針值是一個nil空指針。 我們可以說此指針引用著另外一個值,或者說另外一個值正被此指針所引用。 一個值可能被間接引用,比如

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

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

第二個分類中的類型的(可能的)內部實現結構定義

為了更好地理解第二個分類中的類型的值的運行時刻行為,我們可以認為這些類型在內部是使用第一個分類中的類型來定義的(如下所示)。 如果你以前并沒有很多使用過Go中各種類型的經驗,目前你不必深刻地理解這些定義。 對這些定義擁有一個粗糙的印象足夠對理解后續(xù)文章中將要講解的類型有所幫助。 你可以在今后有了更多的Go編程經驗之后再重讀一下本文。

映射、通道和函數類型的內部定義

映射、通道和函數類型的內部定義很相似:

// 映射類型
type _map *hashtableImpl // 目前,官方標準編譯器是使用
                         // 哈希表來實現映射的。

// 通道類型
type _channel *channelImpl

// 函數類型
type _function *functionImpl

從這些定義,我們可以看出來,這三個種類的類型的內部結構其實是一個指針類型。 或者說,這些類型的值的直接部分在內部是一個指針。 這些類型的每個值的直接部分引用著它的具體實現的底層間接部分。

切片類型的內部定義

切片類型的內部定義:

type _slice struct {
	elements unsafe.Pointer // 引用著底層的元素
	len      int            // 當前的元素個數
	cap      int            // 切片的容量
}

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

字符串類型的內部結構

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

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

接口類型的內部定義

我們可以認為接口類型在內部是如下定義的:

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

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

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

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

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

在賦值中,底層間接值部將不會被復制

現在我們了解了第二個分類中的類型的內部結構是一個指針持有(指針或者指針包裹)類型。 這對于我們理解Go中的值復制行為有很大幫助。

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

值復制

事實上,對于字符串值和接口值的賦值,上述描述在理論上并非百分百正確。 官方FAQ明確說明了在一個接口值的賦值中,接口的底層動態(tài)值將被復制到目標值。 但是,因為一個接口值的動態(tài)值是只讀的,所以在接口值的賦值中,官方標準編譯器并沒有復制底層的動態(tài)值。這可以被視為是一個編譯器優(yōu)化。 對于字符串值的賦值,道理是一樣的。所以對于官方標準編譯器來說,上一段的描述是100%正確的。

因為一個間接值部可能并不專屬于任何一個值,所以在使用unsafe.Sizeof函數計算一個值的尺寸的時候,此值的間接部分所占內存空間未被計算在內。

關于術語“引用類型”和“引用值”

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

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

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


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號