Go 部落格
資料塊
介紹
要透過網路傳輸資料結構或將其儲存到檔案中,必須對其進行編碼,然後再進行解碼。當然,有許多可用的編碼:JSON、XML、Google 的 protocol buffers 等等。現在,Go 的 gob 包又提供了另一種。
為什麼定義一種新的編碼?這工作量很大,而且很冗餘。為什麼不直接使用現有的格式呢?嗯,首先,我們確實使用了!Go 有包支援所有剛才提到的編碼(protocol buffer 包在一個單獨的倉庫中,但它是下載最頻繁的包之一)。對於許多目的,包括與用其他語言編寫的工具和系統通訊,它們是正確的選擇。
但對於 Go 特定的環境,例如用 Go 編寫的兩個伺服器之間的通訊,有機會構建一個更容易使用且可能更高效的東西。
Gobs 以一種外部定義、語言無關的編碼無法做到的方式與語言協同工作。同時,也可以從現有系統中吸取經驗。
目標
gob 包在設計時考慮了許多目標。
首先,也是最明顯的,它必須非常易於使用。首先,因為 Go 具有反射機制,所以不需要單獨的介面定義語言或“協議編譯器”。資料結構本身就是包弄清楚如何編碼和解碼它所需要的全部資訊。另一方面,這種方法意味著 gobs 與其他語言的協作效果不會那麼好,但這沒關係:gobs 毫不諱言地以 Go 為中心。
效率也很重要。以 XML 和 JSON 為代表的文字表示形式太慢,無法作為高效通訊網路的中心。二進位制編碼是必需的。
Gob 流必須是自描述的。每個 gob 流從頭開始讀取時,都包含足夠的資訊,以便完全不瞭解其內容的代理可以解析整個流。此屬性意味著您將始終能夠解碼儲存在檔案中的 gob 流,即使您早已忘記它代表什麼資料。
從我們使用 Google protocol buffers 的經驗中,還有一些東西需要學習。
Protocol buffer 的缺點
Protocol buffers 對 gobs 的設計產生了重大影響,但有三個特性是故意避免的。(撇開 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 在設計和實現中存在一些複雜性。我們決定在 gobs 中省略它們,轉而採用 Go 簡單但有效的預設規則:除非你另行設定,否則它會具有該型別的“零值”——並且不需要傳輸。
因此,gobs 最終看起來像一種通用、簡化的 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
並獲取一個只有 X 被設定(指向設定為 7 的 int8
變數地址)的 u 值;Z 欄位被忽略——你會把它放在哪裡?在解碼 struct 時,欄位按名稱和相容型別匹配,只有在兩者中都存在的欄位會受到影響。這種簡單的方法巧妙地解決了“可選欄位”問題:隨著型別 T 透過新增欄位而演進,舊的接收方仍然可以處理它們識別的那部分型別。因此,gobs 在沒有任何額外機制或符號的情況下,提供了可選欄位的重要結果——可擴充套件性。
從整數我們可以構建所有其他型別:位元組、字串、陣列、切片、對映,甚至是浮點數。浮點值由其 IEEE 754 浮點位模式表示,儲存為整數,只要您知道它們的型別(我們總是知道),這就能很好地工作。順便說一下,該整數是以位元組反轉的順序傳送的,因為浮點數的常見值(例如小整數)在低位有很多零,我們可以避免傳輸這些零。
Go 使 gobs 成為可能的一個很好的特性是,它們允許您透過讓您的型別實現 GobEncoder 和 GobDecoder 介面來定義自己的編碼,其方式類似於 JSON 包的 Marshaler 和 Unmarshaler 以及 fmt 包的 Stringer 介面。此功能使得在傳輸資料時可以表示特殊特性、強制執行約束或隱藏秘密。詳情請參見文件。
線上傳輸的型別
首次傳送給定型別時,gob 包會在資料流中包含該型別的描述。實際上,發生的情況是使用編碼器以標準 gob 編碼格式編碼一個內部 struct,該 struct 描述了型別併為其賦予一個唯一編號。(基本型別以及型別描述結構的佈局由軟體預定義以進行引導。)描述了型別之後,就可以透過其型別編號引用它。
因此,當我們傳送第一個型別 T 時,gob 編碼器會發送 T 的描述併為其標記一個型別編號,例如 127。然後,所有值(包括第一個值)都以該編號為字首,因此 T 值的流看起來像
("define type id" 127, definition of type T)(127, T value)(127, T value), ...
這些型別編號使得描述遞迴型別併發送這些型別的值成為可能。因此,gobs 可以編碼樹等型別
type Node struct {
Value int
Left, Right *Node
}
(如何利用零值預設規則使其工作,即使 gobs 不表示指標,這是留給讀者的一個練習。)
有了型別資訊,除了引導型別集(這是一個明確的起點)之外,gob 流是完全自描述的。
編譯一臺機器
首次編碼給定型別的值時,gob 包會構建一個專門用於該資料型別的小型直譯器機器。它利用型別的反射來構建該機器,但機器一旦構建完成就不再依賴反射。該機器使用 unsafe 包和一些技巧以高速將資料轉換為編碼位元組。它可以使用反射並避免 unsafe,但會顯著變慢。(Go 對 protocol buffer 的支援也採用了類似的高速方法,其設計受到了 gobs 實現的影響。)相同型別的後續值使用已編譯的機器,因此可以立即進行編碼。
[更新:自 Go 1.4 起,gob 包不再使用 unsafe 包,效能略有下降。]
解碼類似但更難。解碼值時,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 包基於 gobs 構建,將這種編碼/解碼自動化轉化為跨網路方法呼叫的傳輸方式。這是另一篇文章的主題。
詳情
gob 包文件,尤其是檔案 doc.go,擴充套件了此處描述的許多細節,幷包含一個完整的示例,展示了編碼如何表示資料。如果您對 gob 實現的內部機制感興趣,那是一個很好的起點。
下一篇文章:Godoc:為 Go 程式碼編寫文件
上一篇文章:C? Go? Cgo!
部落格索引