概要
encoding/gobについて調べてみました。interface{}はencoding/gobでシリアライズしてバイト列にできます。
Gobとは
Go言語専用のシリアライズ形式です。シリアライズ形式というと、バイナリフォーマットで多言語からつかえるProtocol buffersや、RubyのMarshal.dumpで出力されるもの、テキストフォーマットだとJSONやXMLなどが思い当たります。
GobはRPCで通信するときに利用することが想定されていますが、計算途中の結果をファイルに出力するような場合も使えます。また、他のシリアライズ形式と違いGo専用なので、Goで使うなら簡単に使えるという利点があります。
Gobにできて、Protocol buffersにできないこと
参考:Gobs of data
たとえば、Protocol buffersはself-describingでないため、送るデータとは別に構造の定義をDecodeしたいプログラムが知っている必要があります。それ以外に、次のような設計上の違いがあります。
- Top levelのintや、arrayを送ることができない、送るデータは構造体に入れる必要がある
- 途中でフィールドの変更をすることが難しい。厳密にEncodeとDecode側の構造が一致していないと読めない。
- デフォルト値がある、フィールドが無い状態(=nil)にできない。
要するにGoの変数なら、underlying typeでもstructでも、nil(=送らないか明にnilとする)やDynamicな型も含めてそのまま伝えられるという利点があります。ただし、他の言語や人間が読み取る用途には(やればできるでしょうが)向いてません。
Gobのフォーマット
doc.goに詳細はありますが、このような形式です。
(byteCount (-type id, encoding of a wireType)* (type id, encoding of a value))*
wireTypeはstructなどのフォーマットを伝えるための構造です。gobが受け取る新しい構造ごとにtype idが割当たります。その後にtype id
と実際の値のエンコードが続きます。
type wireType struct {
ArrayT *arrayType
SliceT *sliceType
StructT *structType
MapT *mapType
GobEncoderT *gobEncoderType
BinaryMarshalerT *gobEncoderType
TextMarshalerT *gobEncoderType
}
例えば、次のようなデータをエンコードすると、
type Test struct {
Value interface{}
}
var test Test
test.Value = "test"
こうなります。
00000000 1c ff 85 03 01 01 04 54 65 73 74 01 ff 86 00 01 |.......Test.....|
00000010 01 01 05 56 61 6c 75 65 01 10 00 00 00 13 ff 86 |...Value........|
00000020 01 06 73 74 72 69 6e 67 0c 06 00 04 74 65 73 74 |..string....test|
00000030 00 |.|
なお、struct自体の情報はこの構造で表します。
// Struct type
type fieldType struct {
Name string
Id typeId
}
type structType struct {
CommonType
Field []*fieldType
}
順番に意味を説明すると、次のようになります。
値 | 意味 |
---|---|
1c | 全体のサイズ |
ff 85 | -typeid = -67 |
03 | wireTypeの3つめのフィールドStructTの |
01 | 1つめのフィールドCommonType |
01 04 54 65 73 74 | フィールド名が 4文字の"Test" |
01 ff 86 | typeidが67 |
00 | CommonTypeここまで |
01 | []Field |
01 | は1要素 |
01 05 56 61 6c 75 65 | Nameは5文字の"Value |
01 10 | typeid = 8 interface{} の値 |
00 00 00 | wireTypeここまで |
13 ff 86 | 0x13サイズの typeid=67、Testのエンコード値 |
01 | Valueフィールドは |
06 73 74 72 69 6e 67 | "string" |
0c | typeid = 6 string |
06 00 04 74 65 73 74 | 6byte delta 0 (=singleton value) "test" |
00 | Testここまで |
"test"
のようにstructでないものがTop levelにある場合は次のようになります。
(byteCount (type id, zero, encoding of a value))
"test"
のエンコード結果は、次のようになります。
00000000 07 0c 00 04 74 65 73 74 |....test|
値 | 意味 |
---|---|
07 | byteCount |
0c | typeid = 6 string |
00 | delta 0 |
04 74 65 73 74 | "test" |
interface{}を送る
interface{}
を値としてエンコードすると、interface{}
だったという情報が落ちます。すると、デコードするときにこんなエラーが出ます。
gob: local interface type *interface {} can only be decoded from remote interface type; received concrete type int
*interface{}
をエンコードして、*interface{}
で受ければ、OKです。
Register()の使いみち
interface{}
メンバーに入った構造体を贈りたい場合があります。エンコードのときにこんなエラーが出ます。
gob: type not registered for interface: main.SubType
structは前述のようにフィールドをNameとtypeidでしか管理していないため、structが入れ子になっている場合、入れ子の子供側の構造体をRegister()
しないとEncodeされません。
ここはなぜこんな実装になっているか、あまり理解していないです。再帰的に見に行くとキリがない場合が有るのかも?
その他、ありがちなエラー
Encode, Decodeの引数はどんな型でも取れるようになっているので、コンパイル時のチェックが効きません。思った挙動にならない、例えば、Decodeするとnil
になるような場合は戻り値のエラーをprintするといいです。
gob: type mismatch in decoder: want struct type main.Data; got non-struct
この場合は、エンコードしたときの構造体を渡してデコードする必要があります。
attempt to decode into a non-pointer
DecodeValue of unassignable value
Decodeの引数は領域が割当たっているポインタじゃないとデコードしてくれません。
おわりに
このエントリを調べるきっかけは、transparentというライブラリでの2 phase commitの実装です。まだあまり作り込んでませんが、gRPCでgobのバイト列をやり取りする実装になっています。golangの値の受け渡しに使うならencoding/gob
が自由度が高く簡単なので、おすすめです。