LoginSignup
9
3

More than 5 years have passed since last update.

encoding/gobでinterface{}をシリアライズする

Posted at

概要

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
}

例えば、次のようなデータをエンコードすると、

struct
type Test struct {
  Value interface{}
}
value
  var test Test
  test.Value = "test"

こうなります。

encoded
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"のエンコード結果は、次のようになります。

encoded
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が自由度が高く簡単なので、おすすめです。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3