Go 言語ではデータ構造のシリアライズのために標準でいくつかのパッケージが用意されています。
encoding/csv
encoding/json
encoding/xml
等がそれですが、このように言語から完全に独立した汎用形式では表現できない構造をやりとりしたいときがあります。そこで使われるのが encoding/gob
です。
gob って?
上の記事にもう全部書いてあるのですが、gob の特徴として挙げられている以下のような点です。
- とくにかく簡単に使えること
データ構造さえ自明であれば、他のいかなる情報も encode / decode に必要ありません。encode/json
でいう、json:"hoge"
タグなどは必要ないのです。 - 高効率であること
XML や JSON のような、テキストベースの形式は冗長すぎます。gob はバイナリーなのでネットワーク間のやりとりも高速です。
最新のベンチマークを見る限り、encode / decode の速度もまあまあ優秀です。 - 自己記述的であること
encode したデータそれ自体で decode のために必要な情報は全てまかなえます。ファイルに保存しておいて何年も後に取り出すとしても、なんの問題もありません。
まさに Google 自身が同種の用途のために開発した Protocol Buffers というデータ形式があります。先の記事には gob のそれに対する優位性について詳しく述べられていますが、僕は Protocol Buffers を使ったことないのでここでは割愛します。
使ってみる
単純な構造体を encode / decode する
利用法は encoding/json
等とほとんど変わりません。ただ、先にも述べたように json:"hoge"
タグ等の定義が完全に不要なため、それよりは簡潔です。
- 以下、簡便のためエラー処理は端折ります。
- 以下のコードはここで実行出来ます。
// 実行すると以下のように表示される。
// encoded: 46 bytes
// decoded: &{F1:hoge F2:123}
package main
import (
"bytes"
"encoding/gob"
"fmt"
)
type Hoge struct {
F1 string
F2 int64
}
func main() {
encoded := encode()
fmt.Printf("encoded: %d bytes\n", len(encoded))
decoded := decode(encoded)
fmt.Printf("decoded: %+v\n", decoded)
}
func encode() []byte {
h := Hoge{F1: "hoge", F2: 123}
buf := bytes.NewBuffer(nil)
_ = gob.NewEncoder(buf).Encode(&h)
return buf.Bytes()
}
func decode(data []byte) *Hoge {
var h Hoge
buf := bytes.NewBuffer(data)
_ = gob.NewDecoder(buf).Decode(&h)
return &h
}
特に説明は必要ありませんね。encoding/json
と違うところは json.Marshal() / json.Unmarshal()
のような便利メソッドが存在しないことです。
独自の encoder / decoder を定義する
private fields みたいな通常 encode 出来ないヤツは GobEncoder / GobDecoder
interface を使います。json.Marshaler / json.Unmarshaler
と同じです。
- ここでは encoder / decoder の定義のために “Alias” テクニックを使っています。
- 以下のコードはここで実行出来ます。
type Hoge struct {
F1 string
F2 int64
f3 int64 // private fields なので通常の Encoder ではこれが見えない
}
func (h *Hoge) GobEncode() ([]byte, error) {
buf := bytes.NewBuffer(nil)
// Alias 型に F3 というダミーのフィールドを作ってそこに格納する
type Alias Hoge
_ = gob.NewEncoder(buf).Encode(struct {
F3 int64
*Alias
}{F3: h.f3, Alias: (*Alias)(h)})
return buf.Bytes(), nil
}
func (h *Hoge) GobDecode(data []byte) error {
buf := bytes.NewBuffer(data)
// 同じく F3 というダミーのフィールドを作って、そこに値が入る
type Alias Hoge
aux := struct {
F3 int64
*Alias
}{Alias: (*Alias)(h)}
_ = gob.NewDecoder(buf).Decode(&aux)
// ダミーのフィールドから転記する
h.f3 = aux.F3
return nil
}
interface を encode / decode する
今までの例だと JSON / XML を使ったときと余り違いが分かりません。gob の優れている点は interface の扱いです。JSON 等への encode ではそもそも型の情報が抜けてしまうため encode 自体が不可能な場合があります(複雑な Marshaller を定義すれば別ですが)。
type Hoge struct {
Walkers []Walker
}
type Walker interface {
Walk()
}
type Cat string
func (c Cat) Walk() { fmt.Printf("a cat %s is walking...\n", c) }
type Dog string
func (d Dog) Walk() { fmt.Printf("a dog %s is walking...\n", d) }
Hoge
は Walker
という interface を持った何らかの実体を中に収めた構造体です。これを encoding/json
を使って encode / decode しようとしてもうまく行きません。
- コード例はこちら。
// Walker なんてよく分からないものは無理です!的なエラー
panic: json: cannot unmarshal string into Go struct field Hoge.Walkers of type main.Walker
こんなときにこそ gob ……なのですが、流石にノーヒントでは無理です。encoding/json のコード例を s/json/gob/g
しただけだと以下のようなエラーを吐いて止まってしまいます。
// Cat ってヤツのヒントください!的なエラー
panic: gob: type not registered for interface: main.Cat
ヒントは gob.Register()
という関数で与えることが出来ます。例えば以下のような感じです。
gob.Register(Cat(""))
gob.Register(Dog(""))
ヒントを与えた完全な例では以下のように encode / decode がうまく行ったことが確認できます。
original: main.Hoge{Walkers:[]main.Walker{"タマ", "ポチ"}}
decoded: main.Hoge{Walkers:[]main.Walker{"タマ", "ポチ"}}
// 各の Walk() を呼び出してみると型に応じて文章が変わっている
a cat タマ is walking...
a dog ポチ is walking...
きちんと型が認識されていることが分かりますね。interface をそのまま扱えるのは gob の大きな利点です。
まとめ(どういう場合に使う?)
多少の手間(gob.Register()
によるヒント)は必要ですが、Go のデータ構造をそのままやりとりできるのは大きな魅力です。例えばマイクロサービス同士でデータを交換したいけど、JSON 等の汎用的な形式で間に合わない場合に良いでしょう。
少々ニッチな用途ですが、GAE/Go ではこれがかなり有用です。なんとならば、GAE/Go では GOMAXPROCS が 1 のため、CPU bound な処理を並列化できないのです。このとき各処理を別の endpoint で動作可能にし、必要なデータを gob でやりとりすることで無理矢理並列化できます。具体的に何をどうやったのかは又別の記事にまとめたいと思います。