Go 部落格
大量資料
引言
要透過網路傳輸資料結構或將其儲存在檔案中,必須先對其進行編碼,然後再進行解碼。當然,有許多可用的編碼方式:JSON、XML、Google 的 protocol buffers 等等。現在又多了一種,由 Go 的 gob 包提供。
為什麼要定義一種新的編碼方式?這很費力,而且是多餘的。為什麼不直接使用現有的格式之一呢?嗯,首先,我們確實在使用!Go 擁有支援所有這些已提及編碼的包(protocol buffer 包位於單獨的倉庫中,但它是下載次數最多的之一)。對於許多目的,包括與其他語言編寫的工具和系統進行通訊,它們是正確的選擇。
但對於特定於 Go 的環境,例如在兩個用 Go 編寫的伺服器之間進行通訊,就有機會構建一種更易於使用且可能更高效的解決方案。
Gob 的工作方式能夠與語言深度整合,而外部定義的、與語言無關的編碼方式則無法做到。同時,也可以從現有系統中吸取經驗教訓。
目標
gob 包的設計考慮了幾個目標。
首先,也是最明顯的,它必須非常易於使用。首先,由於 Go 具有反射機制,因此無需單獨的介面定義語言或“協議編譯器”。資料結構本身就是包需要了解如何對其進行編碼和解碼的全部資訊。另一方面,這種方法意味著 gob 與其他語言的相容性永遠不如與其他語言無關的編碼方式好,但這沒關係:gob 毫不掩飾地以 Go 為中心。
效率也很重要。以 XML 和 JSON 為例的文字表示對於構成高效通訊網路的中心來說太慢了。二進位制編碼是必不可少的。
Gob 流必須是自描述的。每個 gob 流從頭開始讀取,都包含足夠的資訊,以便能夠由一個事先不知道其內容的代理來解析整個流。這個屬性意味著您將始終能夠解碼儲存在檔案中的 gob 流,即使在您忘記它代表什麼資料很久之後。
從我們使用 Google protocol buffers 的經驗中也吸取了一些教訓。
Protocol buffer 的不足之處
Protocol buffers 對 gob 的設計產生了重大影響,但有三個特性被刻意避免。(暫且不考慮 protocol buffers 不自描述的特性:如果您不知道用於編碼 protocol buffer 的資料定義,您可能無法解析它。)
首先,protocol buffers 只處理我們在 Go 中稱為 struct 的資料型別。您不能在頂層編碼一個整數或陣列,只能編碼一個包含欄位的 struct。這似乎是一種不必要的限制,至少在 Go 中是這樣。如果您只想傳送一個整數陣列,為什麼必須先將其放入一個 struct 中?
接下來,protocol buffer 定義可以指定在編碼或解碼型別 `T` 的值時,欄位 `T.x` 和 `T.y` 必須存在。儘管這種必需欄位可能看起來是個好主意,但它們的實現成本很高,因為編解碼器在編碼和解碼時必須維護一個單獨的資料結構,以便能夠報告必需欄位的缺失。它們也是一個維護問題。隨著時間的推移,您可能希望修改資料定義以刪除必需欄位,但這可能會導致現有客戶端崩潰。最好根本不要在編碼中包含它們。(Protocol buffers 也有可選欄位。但如果我們沒有必需欄位,所有欄位都是可選的,就這樣。稍後將會有更多關於可選欄位的內容。)
第三個 protocol buffer 的不足之處是預設值。如果 protocol buffer 省略了“已預設”欄位的值,那麼解碼後的結構的行為就好像該欄位被設定為該值一樣。這個想法在您擁有 getter 和 setter 方法來控制欄位訪問時效果很好,但在容器只是一個普通的慣用法 struct 時,處理起來更困難。必需欄位的實現也很棘手:預設值在哪裡定義,它們的型別是什麼(文字是 UTF-8?未解釋的位元組?一個浮點數有多少位?)儘管表面上很簡單,但在 protocol buffers 的設計和實現中存在許多複雜性。我們決定將它們排除在 gob 之外,並回退到 Go 中簡單但有效的預設規則:除非您另有設定,否則它具有該型別的“零值”——而且不需要傳輸它。
因此,gob 看起來有點像一種通用化、簡化的 protocol buffer。它們是如何工作的?
值
編碼後的 gob 資料不涉及 `int8` 和 `uint16` 這樣的型別。相反,與 Go 中的常量有些類似,其整數值是抽象的、無大小的數字,有符號或無符號。當您編碼一個 `int8` 時,它的值將作為無大小、可變長度的整數傳輸。當您編碼一個 `int64` 時,它的值也將作為無大小、可變長度的整數傳輸。(有符號和無符號被區別對待,但同樣的無大小性也適用於無符號值。)如果兩者都有值 7,那麼線上傳輸的位元將是相同的。當接收者解碼該值時,它會將其放入接收者的變數中,該變數可以是任意整數型別。因此,編碼器可以傳送一個來自 `int8` 的 7,但接收者可以將其儲存在 `int64` 中。這沒關係:該值是一個整數,只要它能容納,一切都會正常工作。(如果它不適合,則會發生錯誤。)這種與變數大小的分離使得編碼具有一定的靈活性:我們可以隨著軟體的演變而擴充套件整數變數的型別,但仍然能夠解碼舊資料。
這種靈活性也適用於指標。在傳輸之前,所有指標都會被展平。型別 `int8`、`*int8`、`**int8`、`****int8` 等的值都將作為整數值傳輸,該整數值可以儲存在任何大小的 `int`、`*int` 或 `******int` 等中。同樣,這提供了靈活性。
當解碼 struct 時,由於這些欄位僅由編碼器傳送,因此靈活性也體現在目標變數中。鑑於以下值:
type T struct{ X, Y, Z int } // Only exported fields are encoded and decoded.
var t = T{X: 7, Y: 0, Z: 8}
對 `t` 的編碼只發送 7 和 8。由於 `Y` 的值為零,因此甚至沒有傳送;不需要傳送零值。
接收者也可以選擇將該值解碼到以下結構中:
type U struct{ X, Y *int8 } // Note: pointers to int8s
var u U
並獲得一個 `u` 值,其中只有 `X` 被設定(指向一個設定為 7 的 `int8` 變數的地址);`Z` 欄位被忽略——您能把它們放在哪裡呢?在解碼 struct 時,欄位是按名稱和相容型別匹配的,只有同時存在的欄位才會受到影響。這種簡單的方法解決了“可選欄位”問題:隨著型別 `T` 透過新增欄位而演變,過時的接收者仍然可以處理它們識別的部分型別。因此,gob 在沒有任何額外機制或符號的情況下,提供了可選欄位的重要結果——可擴充套件性。
從整數我們可以構建所有其他型別:位元組、字串、陣列、切片、對映,甚至是浮點數。浮點數值由其 IEEE 754 浮點數位模式表示,儲存為整數,只要知道它們的型別(我們總是知道),就可以正常工作。順便說一下,這個整數是以位元組反轉的順序傳送的,因為浮點數的常見值,例如小整數,在低位有很多零,我們可以避免傳輸它們。
gob 的一個不錯的功能是 Go 使之成為可能,即允許您透過讓您的型別滿足 GobEncoder 和 GobDecoder 介面來定義您自己的編碼,這與 JSON 包的 Marshaler 和 Unmarshaler 以及 fmt 包的 Stringer 介面類似。此功能可以表示特殊功能、強制約束或在傳輸資料時隱藏秘密。有關詳細資訊,請參閱文件。
線上的型別
第一次傳送給定型別時,gob 包會在資料流中包含該型別的描述。實際上,發生的情況是,編碼器用於以標準 gob 編碼格式編碼一個描述該型別併為其分配唯一編號的內部 struct。(基本型別以及型別描述結構體的佈局由軟體預定義,以進行自舉。)在型別描述之後,可以透過其型別編號進行引用。
因此,當我們傳送第一個型別 `T` 時,gob 編碼器會發送 `T` 的描述並標記一個型別編號,比如 127。所有值,包括第一個,都會被該編號加上字首,因此 `T` 值的流看起來像:
("define type id" 127, definition of type T)(127, T value)(127, T value), ...
這些型別編號使得描述遞迴型別和傳送這些型別的值成為可能。因此,gob 可以編碼樹等型別。
type Node struct {
Value int
Left, Right *Node
}
(讀者可以自行推斷零值規則如何實現這一點,儘管 gob 不表示指標。)
透過型別資訊,除了一組自舉型別(這是一個明確定義的起點)之外,gob 流是完全自描述的。
編譯一個機器
第一次編碼給定型別的值時,gob 包會為該資料型別構建一個小的解釋型機器。它使用型別的反射來構建該機器,但一旦機器構建完成,它就不再依賴於反射。該機器使用 `unsafe` 包和一些技巧以高速將資料轉換為編碼後的位元組。它也可以使用反射並避免 `unsafe`,但速度會明顯慢一些。(Go 的 protocol buffer 支援也採用了類似的超高速方法,其設計受到了 gob 實現的影響。)後續相同型別的值會使用已編譯好的機器,因此可以立即進行編碼。
[更新:截至 Go 1.4,`unsafe` 包不再由 gob 包使用,效能略有下降。]
解碼是類似的,但更難。當您解碼一個值時,gob 包會持有一個表示給定編碼器定義型別的位元組切片,以及一個用於解碼該值的 Go 值。gob 包會為該對構建一個機器:線上傳輸的 gob 型別與為解碼提供的 Go 型別進行交叉。但是,一旦該解碼機器構建完成,它仍然是一個無反射的引擎,使用 `unsafe` 方法來獲得最大的速度。
用法
在底層有很多工作要做,但結果是一個高效、易於使用的傳輸資料編碼系統。這是一個完整的示例,展示了不同的編碼和解碼型別。請注意傳送和接收值的簡單性;您只需要將值和變數提供給 gob 包,它就會完成所有工作。
package main
import (
"bytes"
"encoding/gob"
"fmt"
"log"
)
type P struct {
X, Y, Z int
Name string
}
type Q struct {
X, Y *int32
Name string
}
func main() {
// Initialize the encoder and decoder. Normally enc and dec would be
// bound to network connections and the encoder and decoder would
// run in different processes.
var network bytes.Buffer // Stand-in for a network connection
enc := gob.NewEncoder(&network) // Will write to network.
dec := gob.NewDecoder(&network) // Will read from network.
// Encode (send) the value.
err := enc.Encode(P{3, 4, 5, "Pythagoras"})
if err != nil {
log.Fatal("encode error:", err)
}
// Decode (receive) the value.
var q Q
err = dec.Decode(&q)
if err != nil {
log.Fatal("decode error:", err)
}
fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}
您可以在 Go Playground 中編譯並執行此示例程式碼。
`rpc 包` 構建在 gob 之上,將這種編碼/解碼自動化轉化為網路上方法呼叫的傳輸。這是另一篇文章的主題。
詳細資訊
`gob 包文件`,特別是 `doc.go` 檔案,詳細介紹了此處描述的許多細節,幷包含一個完整的示例,展示了編碼如何表示資料。如果您對 gob 實現的內部原理感興趣,那是一個不錯的起點。
下一篇文章: Godoc:為 Go 程式碼編寫文件
上一篇文章: C?Go?Cgo!
部落格索引