Gob的數據

譯文:  http://www.mikespook.com/2011/03/%E3%80%90%E7%BF%BB%E8%AF%91%E3%80%91gob-%E7%9A%84%E6%95%B0%E6%8D%AE/ 

原文在此:http://blog.golang.org/2011/03/gobs-of-data.html,來自 Golang 官方博客。

Gob 是 Golang 的包中帶的一個數據結構序列化的編/解碼工具。在實際應用中,已經有不少的編解碼工具/包/庫了,爲什麼 Golang 還要新開發一個 Gob?又是一個重複的輪子?Gob 做了哪些工作?Gob 的優勢是什麼?本文做了一個較爲全面的解釋。

—————-翻譯分割線—————-

Gob 的數據

爲了讓某個數據結構能夠在網絡上傳輸或能夠保存至文件,它必須被編碼然後再解碼。當然,已經有許多可用的編碼方式了:JSON,XML,Google 的 protocol buffers,等等。而現在,又多了一種,由 Go 的 gob 包提供的方式。

爲什麼定義新的編碼?這要做許多繁重的工作。爲什麼不使用某個現成的格式?呃,無論如何,我們這樣做了!Go 已經有剛纔提到的所有編碼方式的包(protocol buffer 包在另外一個代碼庫中,但它是下載得最多的包之一)。並且在許多情況下,包括同其他語言編寫的工具和系統通訊,這些都是正確的選擇。

但是在特定的 Go 環境中,例如在兩個 Go 編寫的服務之間通訊,這需要某些東西使得其更加容易使用,並且可能更加有效率。

Gobs 協同 Go 語言的工作方式,對於那些外部定義的、同語言無關的編碼方式來說無法做到。同時,從現有的系統中也吸取了很多教訓。

目標

gob 包有在設計時有許多目標。

首先,也是最顯然的,它被設計成爲非常容易使用的。一方面,由於 Go 有反射(reflection),就沒有必要弄一個單獨的接口定義語言或“協議編譯器”。數據結構本身提供了編碼和解碼所需要的全部信息。 另一方面,這種方法也意味着 gob 永遠無法良好的同其他語言協同工作,但這沒問題:gob 是厚顏無恥的以 Go 爲中心(譯註:呃,以XXX爲中心,堅決貫徹XXX的領導……)。

效率也是非常重要的。基於文本形式的,如 XML 和 JSON ,應用於高效通訊網絡會太慢了。二進制編碼是必須的(譯註:二進制神馬的,是必須的!迴音:必須的……)!

Gob 流必須可以自解釋。每個 gob 流,從開始讀取,整個流將由包含足夠的信息,以便在終端對其內容毫不知情的前提下對整個流加以解析。這一特性意味,即便你忘記了保存在文件中的 gob 流表示什麼,也總是可以對其解碼。

同樣,這裏有一些從 Google protocol buffers 獲得的經驗。

Protocol buffer 的硬傷

Protocol buffers 對 gob 的設計產生了主要的影響,但是有三個特性被謹慎的避開了。(暫且不說 protocol buffer 不是自解釋的:如果你不知道 protocol buffer 編碼時的數據的定義,你就無法解析它。)

首先,protocol buffer 僅工作於 Go 的 struct 數據類型。不能在最頂級編碼一個整數或者數組,只可以將其至於 struct 中作爲一個字段。 至少在 Go 中,這個限制似乎沒有什麼意義。如果你希望傳輸的僅僅是一個數組或者整數,爲什麼你要先將其放到 struct 中?

其次,可能 protocol buffer 的定義指定字段 T.x 和 T.y 需要解析,無論是在編碼還是解碼類型 T 的值。雖然,這樣的必須字段看起來是個好主意,但是由於編解碼器中必須含有用於編碼和解碼的特定的數據結構,用於報告必須字段是否丟失,實現的開銷是大的。這同樣也產生了問題。過一段時間後,某人可能希望修改數據定義,移除了必須的字段,但這導致現有接收數據的客戶端崩潰。最好是在編碼時就根本沒有這些字段。(Protocol buffer 也有可選字段。但是,如果我們沒有必須字段,所有的字段就是可選的。等一下還會針對可選字段進行一些討論。)

第三個 protocol buffer 的硬傷是默認值。當 protocol buffer 在某個“默認”字段上設置了默認值,而解碼後的結構就像那個字段被設置了某個值一樣。這個想法在有 getter 和 setter 控制字段的訪問的時候非常棒,但是當容器是一個原始結構的時候就很難控制其保持清晰了。必須的字段也存在同樣的麻煩:在哪定義默認值,它們的類型是什麼(是UTF-8文本?無符號字節串?在浮點型中有幾位?)儘管有許多看起來很簡單,protocol buffer 的設計和實現還是有許多伴隨的問題。我們決定讓這些都遠離 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 } // 只有導出字段(exported fields)被編碼和解碼。 var t = T{X: 7, Y: 0, Z: 8} 

編碼後僅發送 7 和 8。由於爲零,Y 不會被髮送;沒有必要發送一個零值。

接收方可能用下面的結構解碼:

type U struct { X, Y *int8 } // 注意:int8 的指針 var u U 

而獲得的 u 的值只有 X (值爲 7 的 int8 變量的地址);Z 字段被忽略了——你應將其放到哪裏呢?當解碼一個 struct 的時候,字段會匹配其名字和類型,只有雙方都有的字段會生效。這個簡單的辦法巧妙處理了“可選字段”問題:類型 T 添加了字段,過期的接收者仍然能處理它們知道的那部分。因此 gob 在可選字段上提供了重要的特性——無須任何額外的機制或標識。

從整數串可以構造其他類型:字節數組、字符串、數組、內存片段、Map,甚至浮點數組。IEEE 754 浮點位定義描述了浮點值存儲爲整數,在你知道其類型的時候,這會工作得很好,我們總是知道類型的吧。另外,這裏的整數使用字節翻轉的順序發送,因爲一般的浮點數字,就像是小整數數組,在低位上有許多個零是不用傳遞的。

gob 還有一個非常棒的特性是 Go 使得通過 GobEncoder 和 GobDecoder 接口使得自定義類型的編碼成爲可能,從某個意義上說類似於 JSON 包的 Marshaler 和 Unmarshaler,以及 fmt 包的 String 化接口。這個技巧使一些特殊功能成爲可能,強制使用常量,或者傳輸數據的時候隱藏信息。閱讀文檔瞭解更多細節。

類型的傳輸

在第一次傳輸給定類型的時候,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,但是會明顯變慢。(受到 gob 實現的影響,Go 的 protocol buffer 使用了類似的機制提高速度。)而後的同樣類型的值使用已經編譯好的翻譯機,這樣就可以總是有一致的編碼。

解碼類似,但是略微複雜。當你解碼一個數據,gob 包用一個字節片保存編碼後的類型的值用於來解碼,再加上得到解碼的 Go 的值。gob 包構造一個翻譯機用於這個過程:gob 類型在線傳輸用於 Go 類型的解碼。一旦解碼翻譯機構造,一個沒有反射的使用 unsafe 方法的引擎能提供最快的速度。

使用

在帽子裏還有很多祕密,但是結果是得到一個用於數據傳輸的高效的,容易使用的編碼系統。這裏有一個完整的例子演示了不同類型的編碼和解碼。留意發送和接收數據是多麼簡單;你所需要做的一切,就是將值和變量置入 gob 包,然後它會完成所有的工作。

package main
import (
    "bytes"
    "fmt"
    "encoding/gob"
    "log"
)  
type P struct {
     X, Y, Z int
     Name string
}
  
type Q struct {
     X, Y *int32
     Name string 
}  

func main() {
     // 初始化編碼器和解碼器。通常 enc 和 dec 會綁定到網絡連接,而編碼器和解碼器會運行在不同的進程中。
     var network bytes.Buffer 
     //代替網絡連接     
     enc := gob.NewEncoder(&network) 
     // 將會寫入網絡     
     dec := gob.NewDecoder(&network) 
     // 將會從網絡中讀取     
     // 編碼(發送)     
     err := enc.Encode(P{3, 4, 5, "Pythagoras"})
     if err != nil {
         log.Fatal("encode error:", err)
     }     
     // 解碼(接收)
     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 的實現感興趣,這是個不錯的起點。

- Rob Pike, 三月 2011



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章