Go標(biāo)準(zhǔn)庫(kù)encoding/gob的具體使用
1.簡(jiǎn)介
encoding/gob
是 Go 語(yǔ)言標(biāo)準(zhǔn)庫(kù)中一個(gè)用于 Go 數(shù)據(jù)結(jié)構(gòu)與二進(jìn)制流之間序列化和反序列化的制協(xié)議包。
gob 包用來(lái)管理 gob 流,它可以實(shí)現(xiàn)在編碼器(發(fā)送器)和解碼器(接收器)之間進(jìn)行二進(jìn)制數(shù)據(jù)流的發(fā)送,一般用來(lái)傳遞遠(yuǎn)端程序調(diào)用的參數(shù)和結(jié)果,比如 net/rpc 包就有用到這個(gè)。
gob 全稱(chēng) GOlang Binary。go 代表 Go 語(yǔ)言,binary 表示其使用二進(jìn)制編碼(而非 JSON/XML 等文本格式)。
2.基礎(chǔ)
gob 流具有自描述性。流中的每個(gè)數(shù)據(jù)項(xiàng)前面都有一個(gè)前綴(采用一個(gè)預(yù)定義類(lèi)型的集合)指明其類(lèi)型。指針不會(huì)被傳輸,但它們指向的內(nèi)容會(huì)被傳輸;也就是說(shuō),值會(huì)被展平。不允許使用零指針,因?yàn)樗鼈儧](méi)有值。遞歸類(lèi)型可以正常工作,但遞歸值(帶循環(huán)的數(shù)據(jù))存在問(wèn)題。這種情況可能會(huì)有所改變。
要使用 gob,需要先創(chuàng)建一個(gè)編碼器,并向其提供一系列數(shù)據(jù)項(xiàng),這些數(shù)據(jù)項(xiàng)可以是值,也可以是可以解引用到值的地址。編碼器會(huì)確保所有類(lèi)型信息在需要之前都已發(fā)送。在接收端,解碼器會(huì)從已編碼的數(shù)據(jù)流中檢索值,并將其解包到局部變量中。
3.類(lèi)型和值
源和目標(biāo)的值/類(lèi)型不必完全對(duì)應(yīng)。
對(duì)于結(jié)構(gòu)體,如果源中有字段(通過(guò)名稱(chēng)標(biāo)識(shí)),但接收變量中沒(méi)有,則將被忽略。如果接收變量中有字段,但傳輸類(lèi)型或值中沒(méi)有,則目標(biāo)變量中也會(huì)忽略這些字段。如果兩個(gè)變量中都存在同名字段,則它們的類(lèi)型必須兼容。接收方和發(fā)送方都會(huì)執(zhí)行所有必要的間接和解引用操作,以便在 gob 和實(shí)際的 Go 值之間進(jìn)行轉(zhuǎn)換。
例如,一個(gè) gob 類(lèi)型如下:
struct { A, B int }
可以從以下任意 Go 類(lèi)型發(fā)送或接收:
struct { A, B int } // 相同 *struct { A, B int } // 結(jié)構(gòu)的額外間接 struct { *A, **B int } // 字段的額外間接 struct { A, B int64 } // 不同的具體值類(lèi)型;見(jiàn)下文
它也可以被接收進(jìn)以下任何一個(gè):
struct { A, B int } // 相同 struct { B, A int } // 順序不重要;按名稱(chēng)匹配 struct { A, B, C int } // 忽略額外字段 (C) struct { B int } // 忽略缺失字段(A);數(shù)據(jù)從數(shù)據(jù)流中將被刪除 struct { B, C int } // 忽略缺失字段(A);忽略額外字段(C)。
嘗試接收下面這些類(lèi)型將引發(fā)解碼錯(cuò)誤:
struct { A int; B uint } // 改變 B 的符號(hào) struct { A int; B float } // 改變 B 的類(lèi)型 struct { } // 沒(méi)有共同的字段名稱(chēng) struct { C, D int } // 沒(méi)有共同的字段名稱(chēng)
整數(shù)的傳輸方式有兩種:任意精度有符號(hào)整數(shù)或任意精度無(wú)符號(hào)整數(shù)。
gob 格式不區(qū)分 int8、int16 等整數(shù)類(lèi)型;只區(qū)分有符號(hào)整數(shù)和無(wú)符號(hào)整數(shù)。如下所述,發(fā)送方以變長(zhǎng)編碼發(fā)送值;接收方接收該值并將其存儲(chǔ)在目標(biāo)變量中。浮點(diǎn)數(shù)始終使用 IEEE 754 64 位精度發(fā)送。
有符號(hào)整數(shù)可以被任何有符號(hào)整數(shù)變量接收:int、int16 等;無(wú)符號(hào)整數(shù)可以被任何無(wú)符號(hào)整數(shù)變量接收;浮點(diǎn)值可以被任何浮點(diǎn)變量接收。但是,目標(biāo)變量必須能夠表示該值,否則解碼操作將失敗。
結(jié)構(gòu)體、數(shù)組和切片也受支持。結(jié)構(gòu)體僅對(duì)導(dǎo)出的字段進(jìn)行編碼和解碼。字符串和字節(jié)數(shù)組支持一種特殊且高效的表示形式(見(jiàn)下文)。切片解碼時(shí),如果現(xiàn)有切片具有容量,則切片將進(jìn)行擴(kuò)展;如果容量不足,則分配一個(gè)新數(shù)組。無(wú)論如何,結(jié)果切片的長(zhǎng)度都會(huì)包含解碼后的元素?cái)?shù)量。
通常,如果需要分配內(nèi)存,解碼器會(huì)分配內(nèi)存。如果不需要,它會(huì)使用從流中讀取的值來(lái)更新目標(biāo)變量。解碼器不會(huì)先初始化目標(biāo)變量,因此如果目標(biāo)是復(fù)合值(如 map、struct 或 slice),解碼后的值將按元素合并到現(xiàn)有變量中。
函數(shù)和通道不會(huì)通過(guò) gob 發(fā)送。嘗試在頂層編碼此類(lèi)值將會(huì)失敗。chan 或 func 類(lèi)型的結(jié)構(gòu)體字段將被視為未導(dǎo)出的字段,并被忽略。
4.編碼細(xì)節(jié)
本節(jié)記錄了編碼細(xì)節(jié),這些細(xì)節(jié)對(duì)大多數(shù)用戶(hù)來(lái)說(shuō)并不重要(可以跳過(guò))。詳細(xì)信息按自下而上的方式呈現(xiàn)。
無(wú)符號(hào)整數(shù)的發(fā)送方式有兩種。
如果小于 128,則以包含該值的字節(jié)形式發(fā)送。否則,將以最小長(zhǎng)度的大端字節(jié)序(高字節(jié)優(yōu)先)字節(jié)流的形式發(fā)送,該字節(jié)流包含該值,并在其前面附加一個(gè)字節(jié),該字節(jié)包含字節(jié)數(shù)(取反)。因此,0 的發(fā)送方式為 (00),7 的發(fā)送方式為 (07),256 的發(fā)送方式為 (FE 01 00)。
布爾值在無(wú)符號(hào)整數(shù)內(nèi)進(jìn)行編碼:0 表示假,1 表示真。
有符號(hào)整數(shù) i 被編碼在無(wú)符號(hào)整數(shù) u 中。u 中的位 1 及以上表示其值;位 0 表示在接收時(shí)是否需要對(duì)其進(jìn)行補(bǔ)碼。編碼算法如下:
var u uint if i < 0 { u = (^uint(i) << 1) | 1 // complement i, bit 0 is 1 } else { u = (uint(i) << 1) // do not complement i, bit 0 is 0 } encodeUnsigned(u)
因此,低位類(lèi)似于符號(hào)位,但將其設(shè)為補(bǔ)碼位可以保證最大負(fù)整數(shù)不是特例。例如,-129=^128=(^256>>1)
編碼為 (FE 01 01)。
浮點(diǎn)數(shù)始終以 float64 值的表示形式發(fā)送。該值使用math.Float64bits轉(zhuǎn)換為 uint64 。然后,uint64 會(huì)被字節(jié)反轉(zhuǎn),并作為常規(guī)無(wú)符號(hào)整數(shù)發(fā)送。字節(jié)反轉(zhuǎn)意味著尾數(shù)的指數(shù)和高精度部分先發(fā)送。由于低位通常為零,這可以節(jié)省編碼字節(jié)數(shù)。例如,17.0 僅用三個(gè)字節(jié)編碼 (FE 31 40)。
字符串和字節(jié)切片以無(wú)符號(hào)計(jì)數(shù)的形式發(fā)送,后跟該值的許多未解釋的字節(jié)。
所有其他切片和數(shù)組都以無(wú)符號(hào)計(jì)數(shù)的形式發(fā)送,后跟使用標(biāo)準(zhǔn) gob 編碼遞歸發(fā)送的多個(gè)元素。
映射以無(wú)符號(hào)計(jì)數(shù)的形式發(fā)送,后跟對(duì)應(yīng)數(shù)量的鍵值對(duì)和元素對(duì)。發(fā)送的是空但非零的映射,因此如果接收方尚未分配映射,則在接收時(shí)始終會(huì)分配一個(gè),除非發(fā)送的映射為零且不在頂層。
在切片和數(shù)組以及映射中,所有元素,甚至是零值元素,都會(huì)被傳輸,即使所有元素都是零。
結(jié)構(gòu)體以 (字段編號(hào),字段值) 對(duì)的序列形式發(fā)送。字段值使用其類(lèi)型的標(biāo)準(zhǔn) gob 編碼遞歸發(fā)送。如果某個(gè)字段的值為其類(lèi)型的零值(數(shù)組除外;參見(jiàn)上文),則該字段將在傳輸中被省略。字段編號(hào)由編碼結(jié)構(gòu)體的類(lèi)型定義:編碼類(lèi)型的第一個(gè)字段為字段 0,第二個(gè)字段為字段 1,依此類(lèi)推。在對(duì)值進(jìn)行編碼時(shí),為了提高效率,字段編號(hào)會(huì)進(jìn)行增量編碼,并且字段始終按字段編號(hào)遞增的順序發(fā)送;因此增量是無(wú)符號(hào)的。增量編碼的初始化將字段編號(hào)設(shè)置為 -1,因此值為 7 的無(wú)符號(hào)整數(shù)字段 0 將被傳輸為無(wú)符號(hào)增量 = 1、無(wú)符號(hào)值 = 7 或 (01 07)。最后,在所有字段都發(fā)送完畢后,一個(gè)終止標(biāo)記表示結(jié)構(gòu)體的結(jié)束。該標(biāo)記是一個(gè)增量 = 0 的值,其表示形式為 (00)。
接口類(lèi)型不進(jìn)行兼容性檢查;所有接口類(lèi)型在傳輸時(shí)都被視為單個(gè)“接口”類(lèi)型的成員,類(lèi)似于 int 或 []byte ——實(shí)際上它們都被視為 interface{}。接口值以字符串形式傳輸,該字符串標(biāo)識(shí)要發(fā)送的具體類(lèi)型(該名稱(chēng)必須通過(guò)調(diào)用 Register 預(yù)定義),后跟表示后續(xù)數(shù)據(jù)長(zhǎng)度的字節(jié)數(shù)(因此,如果無(wú)法存儲(chǔ)該值,則可以跳過(guò)該值),然后是對(duì)存儲(chǔ)在接口值中的具體(動(dòng)態(tài))值的常規(guī)編碼。(nil 接口值由空字符串標(biāo)識(shí),不傳輸任何值。)解碼器接收后,會(huì)驗(yàn)證解包后的具體項(xiàng)是否滿(mǎn)足接收變量的接口要求。
如果將一個(gè)值傳遞給 Encoder.Encode,并且其類(lèi)型不是結(jié)構(gòu)體(或指向結(jié)構(gòu)體的指針等),為了簡(jiǎn)化處理,它會(huì)被表示為一個(gè)包含一個(gè)字段的結(jié)構(gòu)體。這樣做唯一可見(jiàn)的效果是在值之后編碼一個(gè)零字節(jié),就像在已編碼結(jié)構(gòu)體的最后一個(gè)字段之后一樣,這樣解碼算法就能知道頂級(jí)值何時(shí)完成。
類(lèi)型的表示如下所述。當(dāng)在編碼器 (Encoder) 和解碼器 (Decoder) 之間的給定連接上定義類(lèi)型時(shí),它會(huì)被分配一個(gè)有符號(hào)整數(shù)類(lèi)型 ID。當(dāng)調(diào)用 Encoder.Encode(v)
時(shí),它會(huì)確保為 v 的類(lèi)型及其所有元素分配一個(gè) ID,然后發(fā)送 (typeid, encoding-v) 對(duì),其中 typeid 是 v 編碼類(lèi)型的類(lèi)型 ID,encoded-v 是值 v 的 gob 編碼。
為了定義類(lèi)型,編碼器選擇一個(gè)未使用的正類(lèi)型 ID,并發(fā)送對(duì) (-type id, encoding-type),其中 encoding-type 是 wireType 描述的 gob 編碼,由以下類(lèi)型構(gòu)成:
type wireType struct { ArrayT *arrayType SliceT *sliceType StructT *structType MapT *mapType GobEncoderT *gobEncoderType BinaryMarshalerT *gobEncoderType TextMarshalerT *gobEncoderType } type arrayType struct { CommonType Elem typeId Len int } type CommonType struct { Name string // the name of the struct type Id int // the id of the type, repeated so it's inside the type } type sliceType struct { CommonType Elem typeId } type structType struct { CommonType Field []fieldType // the fields of the struct. } type fieldType struct { Name string // the name of the field. Id int // the type id of the field, which must be already defined } type mapType struct { CommonType Key typeId Elem typeId } type gobEncoderType struct { CommonType }
如果有嵌套類(lèi)型 ID,則在使用頂級(jí)類(lèi)型 ID 描述編碼 v 之前,必須定義所有內(nèi)部類(lèi)型 ID 的類(lèi)型。
為了簡(jiǎn)化設(shè)置,連接被定義為先驗(yàn)地理解這些類(lèi)型,以及基本 gob 類(lèi)型 int、uint 等。它們的 ID 是:
bool 1 int 2 uint 3 float 4 []byte 5 string 6 complex 7 interface 8 // gap for reserved ids. WireType 16 ArrayType 17 CommonType 18 SliceType 19 StructType 20 FieldType 21 // 22 is slice of fieldType. MapType 23
最后,通過(guò)調(diào)用 Encode 創(chuàng)建的每條消息前面都會(huì)有一個(gè)編碼的無(wú)符號(hào)整數(shù)計(jì)數(shù),表示消息中剩余的字節(jié)數(shù)。在初始類(lèi)型名稱(chēng)之后,接口值也以相同的方式包裝;實(shí)際上,接口值的行為就像是 Encode 的遞歸調(diào)用。
總結(jié)一下,gob 流看起來(lái)像這樣:
(byteCount (-type id, encoding of a wireType)* (type id, encoding of a value))*
其中 * 表示零次或多次重復(fù),并且值的類(lèi)型 ID 必須預(yù)先定義或在流中的值之前定義。
兼容性:此軟件包的任何未來(lái)更改都將盡力保持與使用先前版本編碼的流的兼容。也就是說(shuō),此軟件包的任何發(fā)布版本都應(yīng)該能夠解碼使用任何先前發(fā)布版本寫(xiě)入的數(shù)據(jù),但可能會(huì)受到安全修復(fù)等問(wèn)題的制約。有關(guān)背景信息,請(qǐng)參閱 Go 兼容性文檔:https://golang.org/doc/go1compat。
5.安全
此軟件包并非針對(duì)對(duì)抗性輸入進(jìn)行加固,且不在 https://go.dev/security/policy 的范圍內(nèi)。具體而言,解碼器僅對(duì)解碼后的輸入大小進(jìn)行基本的完整性檢查,且其限制不可配置。解碼來(lái)自不受信任來(lái)源的 gob 數(shù)據(jù)時(shí)應(yīng)格外小心,因?yàn)檫@可能會(huì)消耗大量資源。
6.主要函數(shù)
6.1 注冊(cè)
Register 和 RegisterName 函數(shù)都用于注冊(cè)具體類(lèi)型以實(shí)現(xiàn)接口的編解碼。
- func Register
// Register 記錄 value 具體值對(duì)應(yīng)的類(lèi)型和其名稱(chēng)。 // 該名稱(chēng)將用來(lái)識(shí)別發(fā)送或接受接口類(lèi)型值時(shí)下層的具體類(lèi)型。 // 本函數(shù)只應(yīng)在初始化時(shí)調(diào)用,如果類(lèi)型和名字的映射不是一一對(duì)應(yīng)的,會(huì) panic。 func Register(value any)
- func RegisterName
// RegisterName 與 Register 類(lèi)似,但使用提供的名稱(chēng)而不是類(lèi)型的默認(rèn)名稱(chēng)。 func RegisterName(name string, value any)
func Register 底層會(huì)調(diào)用 func RegisterName。
為什么編解碼接口要提前注冊(cè)具體類(lèi)型呢?
主要原因涉及類(lèi)型安全、編解碼機(jī)制和運(yùn)行時(shí)動(dòng)態(tài)處理的需求。以下是詳細(xì)解釋?zhuān)?/p>
1. 接口的底層類(lèi)型在運(yùn)行時(shí)才能確定
- 接口變量在編譯時(shí)只有抽象類(lèi)型信息(如
Animal
),但實(shí)際存儲(chǔ)的是具體類(lèi)型(如Dog
或Cat
)。 - Gob 在編碼時(shí)需要知道接口背后的具體類(lèi)型,才能正確序列化數(shù)據(jù);解碼時(shí)需要通過(guò)注冊(cè)信息還原出正確的具體類(lèi)型。
- 如果不注冊(cè):解碼器無(wú)法知道應(yīng)該將數(shù)據(jù)還原為
Dog
還是Cat
,導(dǎo)致解碼失敗。
2.類(lèi)型標(biāo)識(shí)的唯一性
- Gob 通過(guò)注冊(cè)機(jī)制為每個(gè)具體類(lèi)型分配唯一的內(nèi)部標(biāo)識(shí)符(如
main.Dog
)。 - 編碼時(shí),Gob 會(huì)寫(xiě)入這個(gè)標(biāo)識(shí)符;解碼時(shí),通過(guò)標(biāo)識(shí)符查找已注冊(cè)的類(lèi)型,并調(diào)用對(duì)應(yīng)的解碼方法。
- 如果不注冊(cè):解碼器無(wú)法將接收到的數(shù)據(jù)映射到正確的 Go 類(lèi)型。
3.安全性與顯式意圖
- 強(qiáng)制注冊(cè)是一種安全機(jī)制,防止意外解碼未預(yù)期的類(lèi)型(類(lèi)似反序列化攻擊)。
- 開(kāi)發(fā)者必須顯式聲明“允許通過(guò)接口傳輸哪些具體類(lèi)型”,避免運(yùn)行時(shí)不可控行為。
4.與結(jié)構(gòu)體的自動(dòng)處理對(duì)比
- 結(jié)構(gòu)體:如果編解碼雙方有相同的類(lèi)型定義,Gob 可以自動(dòng)推導(dǎo)類(lèi)型信息,因?yàn)榻Y(jié)構(gòu)體名稱(chēng)和字段是確定的。
- 接口:具體類(lèi)型是動(dòng)態(tài)的,無(wú)法通過(guò)靜態(tài)分析確定,必須依賴(lài)運(yùn)行時(shí)的注冊(cè)信息。
5.示例分析
type Animal interface { Speak() string } type Dog struct { Name string } type Cat struct { Name string } func init() { gob.Register(Dog{}) // 必須注冊(cè) gob.Register(Cat{}) // 必須注冊(cè) } func send(a Animal) { // 編碼時(shí),Gob 需要知道 a 的具體類(lèi)型是 Dog 還是 Cat // 通過(guò)注冊(cè)表,可以找到 Dog 或 Cat 的類(lèi)型標(biāo)識(shí)符 }
為什么不能像 JSON 那樣自動(dòng)處理?
- JSON 等文本協(xié)議通過(guò)字段名(如
"type": "dog"
)顯式標(biāo)識(shí)類(lèi)型,但 Gob 是二進(jìn)制協(xié)議,設(shè)計(jì)上追求高效和緊湊,不存儲(chǔ)冗余的類(lèi)型描述。 - Gob 的注冊(cè)機(jī)制避免了每次傳輸都附帶完整的類(lèi)型信息,提升了性能。
總結(jié)
Gob 要求注冊(cè)接口的具體類(lèi)型,本質(zhì)上是為了解決接口的動(dòng)態(tài)類(lèi)型特性與二進(jìn)制編解碼的靜態(tài)需求之間的矛盾。這是一種在靈活性、安全性和性能之間的權(quán)衡設(shè)計(jì)。
6.2 編碼
數(shù)據(jù)在傳輸時(shí)會(huì)先經(jīng)過(guò)編碼(序列化)后再進(jìn)行傳輸,與編碼相關(guān)的有三個(gè)方法:
- func NewEncoder
// NewEncoder 返回一個(gè)將在 io.Writer 上傳輸?shù)男戮幋a器。 func NewEncoder(w io.Writer) *Encoder
- func (*Encoder) Encode
// Encode 會(huì)傳輸接口值所表示的數(shù)據(jù)項(xiàng),并保證所有必要的類(lèi)型信息都已傳輸完畢。 // 向 Encoder 傳遞一個(gè) nil 指針會(huì)導(dǎo)致 panic,因?yàn)?gob 無(wú)法傳輸此類(lèi)數(shù)據(jù)。 func (enc *Encoder) Encode(e any) error
- func (*Encoder) EncodeValue
// EncodeValue 傳輸反射值所表示的數(shù)據(jù)項(xiàng),并保證所有必要的類(lèi)型信息都已傳輸完畢。 // 將 nil 指針傳遞給 EncodeValue 會(huì)導(dǎo)致 panic,因?yàn)樗鼈儫o(wú)法通過(guò) gob 傳輸。 func (enc *Encoder) EncodeValue(value reflect.Value) error
6.3 解碼
接收到數(shù)據(jù)后需要對(duì)數(shù)據(jù)進(jìn)行解碼(序列化),與解碼相關(guān)的有三個(gè)方法:
- func NewDecoder
// NewDecoder 返回一個(gè)從 io.Reader 讀取數(shù)據(jù)的新解碼器。 // 如果 r 未實(shí)現(xiàn) io.ByteReader 接口,則會(huì)將其包裝在 bufio.Reader 中。 func NewDecoder(r io.Reader) *Decoder
- func (*Decoder) Decode
// Decode 從輸入流中讀取下一個(gè)值,并將其存儲(chǔ)在空接口值所表示的數(shù)據(jù)中。 // 如果 e 為 nil,則該值將被丟棄。否則,e 的底層值必須是指向下一個(gè)接收數(shù)據(jù)項(xiàng)的正確類(lèi)型的指針。 // 如果輸入位于 EOF,解碼將返回 io.EOF 并且不修改 e。 func (dec *Decoder) Decode(e any) error
- func (*Decoder) DecodeValue
// DecodeValue 從輸入流中讀取下一個(gè)值。 // 如果 v 為零的 reflect.Value(v.Kind() == Invalid),DecodeValue 會(huì)丟棄該值。否則,它會(huì)將值存儲(chǔ)到 v 中。在這種情況下,v 必須表示一個(gè)指向數(shù)據(jù)的非零指針,或者是一個(gè)可賦值的 reflect.Value(v.CanSet())。 // 如果輸入位于 EOF,DecodeValue 會(huì)返回 io.EOF 并且不會(huì)修改 v。 func (dec *Decoder) DecodeValue(v reflect.Value) error
7.示例
7.1 編解碼結(jié)構(gòu)體
package main import ( "bytes" "encoding/gob" "fmt" "log" ) type Person struct { Name string Age int } func main() { // 創(chuàng)建數(shù)據(jù) alice := Person{Name: "Alice", Age: 30} // 序列化 var buf bytes.Buffer encoder := gob.NewEncoder(&buf) if err := encoder.Encode(alice); err != nil { log.Fatal("Encode error:", err) } fmt.Printf("Serialized data: %x\n", buf.Bytes()) // 反序列化 var bob Person decoder := gob.NewDecoder(&buf) if err := decoder.Decode(&bob); err != nil { log.Fatal("Decode error:", err) } fmt.Printf("Deserialized: %+v\n", bob) }
運(yùn)行輸出:
Serialized data: 247f03010106506572736f6e01ff8000010201044e616d65010c00010341676501040000000cff800105416c696365013c00
Deserialized: {Name:Alice Age:30}
7.2 編解碼接口
package main import ( "bytes" "encoding/gob" "fmt" "log" ) type Animal interface { Sound() string } type Dog struct{ Name string } func (d Dog) Sound() string { return "Woof!" } type Cat struct{ Name string } func (c Cat) Sound() string { return "Meow!" } func interfaceExample() { // 注冊(cè)具體類(lèi)型 gob.Register(Dog{}) gob.Register(Cat{}) animals := []Animal{ Dog{Name: "Rex"}, Cat{Name: "Whiskers"}, } // 序列化 var buf bytes.Buffer if err := gob.NewEncoder(&buf).Encode(animals); err != nil { log.Fatal(err) } // 反序列化 var decoded []Animal if err := gob.NewDecoder(&buf).Decode(&decoded); err != nil { log.Fatal(err) } for _, a := range decoded { fmt.Printf("%T: %s says %s\n", a, a.(interface{ GetName() string }).GetName(), a.Sound()) } } // 為類(lèi)型添加GetName方法以便類(lèi)型斷言 func (d Dog) GetName() string { return d.Name } func (c Cat) GetName() string { return c.Name } func main() { interfaceExample() }
運(yùn)行輸出:
main.Dog: Rex says Woof!
main.Cat: Whiskers says Meow!
注意,編解碼接口時(shí)需要提前注冊(cè)具體類(lèi)型,否則會(huì)報(bào)如下錯(cuò)誤:
gob: type not registered for interface: main.Dog
7.3 文件讀寫(xiě)
也可以使用 gob 將序列化后的數(shù)據(jù)持久化到磁盤(pán)文件。
func fileStorage() { type Config struct { APIKey string Port int } cfg := Config{APIKey: "secret123", Port: 8080} // 寫(xiě)入文件 file, err := os.Create("config.gob") if err != nil { log.Fatal(err) } defer file.Close() if err := gob.NewEncoder(file).Encode(cfg); err != nil { log.Fatal(err) } // 從文件讀取 file, err = os.Open("config.gob") if err != nil { log.Fatal(err) } defer file.Close() var loaded Config if err := gob.NewDecoder(file).Decode(&loaded); err != nil { log.Fatal(err) } fmt.Printf("Loaded config: %+v\n", loaded) }
運(yùn)行輸出:
Loaded config: {APIKey:secret123 Port:8080}
7.4 自定義編解碼方式
Gob 可以通過(guò)調(diào)用相應(yīng)的方法(按優(yōu)先順序)對(duì)實(shí)現(xiàn)了 GobEncoder 或 encoding.BinaryMarshaler 接口的任何類(lèi)型的值進(jìn)行編碼。
Gob 可以通過(guò)調(diào)用相應(yīng)的方法(按優(yōu)先順序)對(duì)實(shí)現(xiàn)了 GobDecoder 或 encoding.BinaryUnmarshaler 接口的任何類(lèi)型的值進(jìn)行解碼。
package main import ( "bytes" "encoding/gob" "fmt" "log" ) // Vector 類(lèi)型實(shí)現(xiàn)了BinaryMarshal/BinaryUnmarshal 方法,這樣我們就可以發(fā)送和接受 gob 類(lèi)型的數(shù)據(jù)。 type Vector struct { x, y, z int } func (v Vector) MarshalBinary() ([]byte, error) { // A simple encoding: plain text. var b bytes.Buffer _, _ = fmt.Fprintln(&b, v.x, v.y, v.z) return b.Bytes(), nil } // UnmarshalBinary 修改接收器,所以必須要傳遞指針類(lèi)型 func (v *Vector) UnmarshalBinary(data []byte) error { // A simple encoding: plain text. b := bytes.NewBuffer(data) _, err := fmt.Fscanln(b, &v.x, &v.y, &v.z) return err } // 此示例傳輸實(shí)現(xiàn)自定義編碼和解碼方法的值。 func main() { var network bytes.Buffer // 創(chuàng)建一個(gè)編碼器發(fā)送數(shù)據(jù) enc := gob.NewEncoder(&network) err := enc.Encode(Vector{3, 4, 5}) if err != nil { log.Fatal("encode:", err) } // 創(chuàng)建一個(gè)解碼器接收數(shù)據(jù) dec := gob.NewDecoder(&network) var v Vector err = dec.Decode(&v) if err != nil { log.Fatal("decode:", err) } fmt.Printf("%#v\n", v) }
運(yùn)行輸出:
main.Vector{x:3, y:4, z:5}
8.優(yōu)勢(shì)與局限
8.1 優(yōu)勢(shì)
- Go 原生高性能
gob 使用二進(jìn)制格式進(jìn)行編解碼,性能比 JSON/XML 快 2-5 倍,數(shù)據(jù)體積小 30-70%。
- 零配置自動(dòng)化
自動(dòng)處理復(fù)雜類(lèi)型:
type Complex struct { Slice []*int Map map[string]chan struct{} Func func() // 不支持! } // 自動(dòng)支持:切片、指針、映射、結(jié)構(gòu)體嵌套
- 版本演進(jìn)支持
對(duì)結(jié)構(gòu)體新增、刪除字段或順序調(diào)整有較好的兼容性。
// V1 結(jié)構(gòu) type User struct { ID int; Name string } // V2 結(jié)構(gòu)(添加字段) type User struct { ID int; Name string; Email string } // V1 數(shù)據(jù) → V2 解碼:Email 自動(dòng)置零值 // 舊版本 type Config struct { Host string; Port int } // 新版本(字段調(diào)序)仍可兼容 type Config struct { Port int; Host string }
- 循環(huán)引用處理
type Node struct { Value int Next *Node // 循環(huán)指針 } n1 := &Node{Value: 1} n2 := &Node{Value: 2, Next: n1} n1.Next = n2 // 循環(huán)引用 // Gob 完美序列化/反序列化
8.2 局限
- 跨語(yǔ)言不兼容
gob 是 Golang 自有的二進(jìn)制編解碼方案,與其他語(yǔ)言不兼容。
- 接口類(lèi)型約束
編解碼接口類(lèi)型時(shí),必須預(yù)注冊(cè)。
type Encoder interface { Encode() []byte } func main() { var enc Encoder = MyEncoder{} gob.Register(MyEncoder{}) // 必須! gob.Encode(enc) }
- 結(jié)構(gòu)演進(jìn)限制
破壞性變更不可逆。
變更類(lèi)型 | 是否兼容 | 后果 |
---|---|---|
添加字段 | ? | 新字段為零值 |
刪除字段 | ? | 忽略不存在的字段,正常解碼 |
重命名字段 | ? | 數(shù)據(jù)丟失 |
修改字段類(lèi)型 | ? | 解碼崩潰 |
- 安全風(fēng)險(xiǎn)
反序列化漏洞。
// 不可信來(lái)源的gob數(shù)據(jù)可能: // 1. 導(dǎo)致內(nèi)存耗盡(大容器攻擊) // 2. 觸發(fā)未預(yù)期類(lèi)型重建 // 3. 暴露私有字段(通過(guò)反射)
- 性能邊界
不適合性能要求的極端場(chǎng)景。
場(chǎng)景 | Gob 性能 | 替代方案 |
---|---|---|
100K+ QPS | ?? 中等 | FlatBuffers |
微秒級(jí)延遲要求 | ? 不足 | Cap’n Proto |
移動(dòng)設(shè)備 | ?? 較重 | MessagePack |
8.3 小結(jié)
優(yōu)勢(shì)與局限對(duì)比表:
特性 | 優(yōu)勢(shì) | 局限性 |
---|---|---|
語(yǔ)言支持 | Go原生深度優(yōu)化 | 僅限Go,無(wú)跨語(yǔ)言能力 |
開(kāi)發(fā)效率 | 零配置/自動(dòng)類(lèi)型處理 | 接口需手動(dòng)注冊(cè) |
性能 | 比文本協(xié)議快5倍 | 仍慢于FlatBuffers等零拷貝方案 |
數(shù)據(jù)兼容 | 支持向前擴(kuò)展字段 | 刪除/重命名字段破壞兼容性 |
類(lèi)型系統(tǒng) | 完美支持Go復(fù)雜類(lèi)型 | 不支持func、chan等類(lèi)型 |
安全 | 無(wú)遠(yuǎn)程代碼執(zhí)行風(fēng)險(xiǎn) | 仍可能遭受資源耗盡攻擊 |
調(diào)試便捷性 | 數(shù)據(jù)不可讀(需專(zhuān)用工具) | JSON更易調(diào)試 |
9.最佳實(shí)踐
類(lèi)型兼容性
- 添加新字段到結(jié)構(gòu)體末尾以保持向后兼容。
- 不要?jiǎng)h除或重命名字段。
// 兼容性示例 type V1 struct { A int } type V2 struct { A int B string // 新增字段在末尾 }
注冊(cè)策略
在 init() 函數(shù)中注冊(cè)類(lèi)型。
跨服務(wù)使用 RegisterName 保持名稱(chēng)一致。
安全考慮
- 不要反序列化不可信來(lái)源的數(shù)據(jù)。
- 對(duì)于網(wǎng)絡(luò)傳輸,添加加密/認(rèn)證層。
調(diào)試技巧
// 調(diào)試編碼數(shù)據(jù) fmt.Printf("%x\n", buf.Bytes()) // 或者轉(zhuǎn)換為字符串查看(可能包含可讀內(nèi)容) fmt.Println(buf.String())
替代方案考慮
- 需要跨語(yǔ)言:使用 JSON 或 Protocol Buffers。
- 需要人類(lèi)可讀:使用 JSON。
- 極致性能:考慮 MessagePack 或 FlatBuffers。
通過(guò)這份快速指南,您應(yīng)該能夠立即開(kāi)始使用 encoding/gob
進(jìn)行高效的數(shù)據(jù)序列化操作。對(duì)于大多數(shù) Go 服務(wù)間的通信需求,gob 提供了簡(jiǎn)單高效的解決方案。
參考文獻(xiàn)
到此這篇關(guān)于Go標(biāo)準(zhǔn)庫(kù)encoding/gob的具體使用的文章就介紹到這了,更多相關(guān)Go encoding/gob內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希
- Golang的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù)的整理介紹
- 解析Go 標(biāo)準(zhǔn)庫(kù) http.FileServer 實(shí)現(xiàn)靜態(tài)文件服務(wù)
- go 下載非標(biāo)準(zhǔn)庫(kù)包(部份包被墻了)到本地使用的方法
- Golang標(biāo)準(zhǔn)庫(kù)syscall詳解(什么是系統(tǒng)調(diào)用)
- 深入解析golang中的標(biāo)準(zhǔn)庫(kù)flag
- 關(guān)于Golang標(biāo)準(zhǔn)庫(kù)flag的全面講解
- Golang標(biāo)準(zhǔn)庫(kù)binary詳解
- GoLang語(yǔ)法之標(biāo)準(zhǔn)庫(kù)fmt.Printf的使用
相關(guān)文章
如何使用工具自動(dòng)監(jiān)測(cè)SSL證書(shū)有效期并發(fā)送提醒郵件
本文介紹了如何開(kāi)發(fā)一個(gè)工具,用于每日檢測(cè)SSL證書(shū)剩余有效天數(shù)并通過(guò)郵件發(fā)送提醒,工具基于命令行,通過(guò)SMTP協(xié)議發(fā)送郵件,需配置SMTP連接信息,本文還提供了配置文件樣例及代碼實(shí)現(xiàn),幫助用戶(hù)輕松部署和使用該工具2024-10-10golang中使用proto3協(xié)議導(dǎo)致的空值字段不顯示的問(wèn)題處理方案
這篇文章主要介紹了golang中使用proto3協(xié)議導(dǎo)致的空值字段不顯示的問(wèn)題處理方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)示例代碼
在?Go?語(yǔ)言中,您可以使用?os/exec?包來(lái)執(zhí)行外部命令,不通過(guò)調(diào)用?shell,并且能夠獲得進(jìn)程的退出碼、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤輸出,下面給大家分享golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)的方法,感興趣的朋友跟隨小編一起看看吧2024-06-06Golang?int函數(shù)使用實(shí)例全面教程
這篇文章主要為大家介紹了Golang?int函數(shù)使用實(shí)例全面教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10golang定時(shí)器Timer的用法和實(shí)現(xiàn)原理解析
這篇文章主要介紹了golang定時(shí)器Ticker,本文主要來(lái)看一下Timer的用法和實(shí)現(xiàn)原理,需要的朋友可以參考以下內(nèi)容2023-04-04golang如何實(shí)現(xiàn)mapreduce單進(jìn)程版本詳解
這篇文章主要給大家介紹了關(guān)于golang如何實(shí)現(xiàn)mapreduce單進(jìn)程版本的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01