這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
關於 Go 的記憶體結構在 Go 記憶體模型 中已經有介紹,但是內容相對簡單,許多細節也一帶而過。Ross Cox 的這篇文章 Go Data Structure 講解得比較系統也很全面的一篇。翻譯至此,希望能對大家有協助。
2009 年的舊文,發現自己當時沒有翻譯完。所以再次做了增補和修改。如果我沒記錯,應該已經有人在 OSC 上發表過同一篇文章的翻譯了。大家對照參考閱讀吧。
————翻譯分隔線————
Go 資料結構
每當給新手介紹 Go 的時候,我發現為了建立起關於哪個操作成本更加高昂的正確觀念,將 Go 如何為其值分配記憶體說明清楚會很有協助。本文介紹了基礎類型、結構體、數組和切片(slice)。
基本類型
先來看看幾個簡單的例子:
變數 i
的類型是 int
,在記憶體中表現為一個 32 位的字。(所有圖展示的都為 32 位記憶體結構;在當前的實現裡,在 64 位元的架構中只有指標會變大,int
仍然還是 32 位,不過也可能選擇 64 位元來作為替代實現。)
由於顯式的轉換,變數 j
的類型是 int32
。雖然 i
和 j
有相同的記憶體布局,但是它們是不同的類型:賦值 i = j
會產生一個類型錯誤,因此必須顯式的進行轉換:i = int(j)
。
變數 f
的類型是 float
,當前的實現是 32 位的浮點類型。它的記憶體佔用與 int32
一樣,但內部布局不同。
結構體與指標
接下來,變數 bytes
的類型是 [5]byte
,一個有 5 位元組的數組。它的記憶體表現就是這 5 個位元組,跟 C 的數組一樣一個個挨著。類似的 primes
是一個有 4 個 int
的數組。
Go,更接近 C 而不是 Java,它為程式員提供了是不是指標的權力。例如,這個類型定義:
type Point struct { X, Y int }
定義了一個叫做 Point
的簡單的結構類型,在記憶體中表現為兩個相鄰的 int
。
複合文法語句 Point{10, 20}
對 Point
進行了初始化。對一個複合文法進行取地址表示了一個指向剛剛分配並初始化的 Point
的指標。前者在記憶體中是兩個字;後者是一個指向兩個字的記憶體的指標。
結構體中的欄位在記憶體中是一個挨一個的排布的。
type Rect1 struct { Min, Max Point }type Rect2 struct { Min, Max *Point }
Rect1
,一個有兩個 Point
欄位的結構體,表達成一行有兩個 Point
,或者說四個 int
。Rect2
,一個有兩個 *Point 欄位的結構體,表達成兩個 *Point。
那些使用過 C 的程式員可能不會對 Point 欄位和 *Point 欄位之間的區別感到驚訝,而哪些僅僅使用過 Java 或 Python(以及其他……)可能對決定使用哪種而感到詫異。通過為程式員提供了基本的記憶體布局控制能力,Go 提供了對一組資料結構的整體大小、分配數量和記憶體訪問模式進行控制的能力。所有都是構建能夠良好啟動並執行系統的關鍵。
字串
有了前面這些鋪墊,我們可以繼續瞭解那些更加有趣的資料類型了。
(灰色箭頭表示存在於實現中,但是無法在程式中直接看到的指標。)
一個 string
在記憶體中表現為雙字結構體,包含指向字串資料的指標和其長度。由於 string 是不可變的,因此多個字串共用同一儲存空間是安全的。那麼如果對 s
進行切,片使其成為一個新的雙字結構體,會在內部產生另一個指標和長度,但仍然指向相同的位元組序列。這意味著切片可以在不進行任何分配和複製的情況下完成,因此切片同指定序號輪尋字串同樣有效率。
(從另一方面來說,在 Java 和其他語言中將字串切片到更小的片段時,有一個眾所周知的問題,即便是只有一個小片段被使用的情況下,原始的引用都將在記憶體中保留整個原始字串。Go 也有同樣的問題。我們已經嘗試但拒絕了一個使用分配和複製的替代方案,這個方案會讓字串切片的成本更加高昂,大多數程式都希望避免這一情況。)
slice
一個 slice 是指向一個數組的某個片段的引用。在記憶體中,它是一個三字結構體,包含了指向首元素的指標、slice 的長度和容量。長度是類似 x[i]
這樣的索引操作的上限,而容量是 x[i:j]
這樣的切片操作的上限。
與對字串切片一樣,對數組切片也不會產生複製:它僅僅建立一個新的用於儲存不同的指標、長度和容量的結構體。在這個例子中,複合文法 []int{2, 3, 5, 7, 11}
建立了一個包含有五個值的新數組,然後設定了 slice x
的欄位來描述這個數組。slice 運算式 x[1:3] 沒有分配任何資料:它只是填充了一個指向相同底層儲存的新的 slice 結構體。在例子中,長度為 2,y[0]
和 y[1]
是唯一合法的序號;而容量是 4,y[0:4]
是一個合法的 slice 運算式。(參閱 Effective Go 瞭解更多關於 slice 長度和容量,以及如何使用的內容。)
由於 slice 是一個多字結構體,在沒有指標的情況下,切片操作不需要分配記憶體,甚至是 slice 頭也不需要,它通常儲存在棧上。這使得 slice 的使用與在 C 中傳遞指定的指標和長度的成本一樣低廉。Go 最初將 slice 作為一個指向上面展示的結構體的指標,但是這樣的話意味著每一個切片操作都會分配新的記憶體對象。即便使用快速分配也為記憶體回收行程產生了許多額外的工作。我們發現了這一情況,就像前面在字串部分已經提及的,這種情況下程式可能會避免切片操作而使用輪尋。移除了這些間接量與記憶體配置,使得 slice 的成本已經足夠低廉,在大多數情況下都不需要輪尋了。
new 和 make
Go 有兩個資料結構建立函數:new
和 make
。它們的區別最初可能引起混淆,不過很快就會感到正常。最基本的區別是 new(T)
返回一個 *T
,一個 Go 程式可以隱式拋棄的指標(圖中黑色箭頭),但 make(T, args)
返回一個原始的 T
而不是指標。通常 T
有其內部隱式實現的指標(圖中灰色的箭頭)。new
返回一個指向空值填充的記憶體,而 make
返回一個複雜的結構體。
有一種辦法可以將這兩種情況統一起來,不過可能會顛覆從 C 和 C++ 而來的傳統:定義 make(*T) 來返回一個指向新分配的 T 的記憶體,那麼當前 new(Point) 可以寫為 make(*Point)。我們對此嘗試了幾天,但是覺得這與人們通常希望的記憶體配置函數實在大相徑庭。
即將來臨
這已經夠長了。介面值、map 和 channel 將只能等待以後的文章了。