Gob vs JSON

公開:2020/10/03
更新:2020/10/03
3 min読了の目安(約3100字TECH技術記事

JSONよりGobの方が…

データのシリアライズのフォーマットとしてはJSONがよく使われるが、受取先もGoならGobというパッケージが使える。しかもJSONよりシリアライズされたデータのサイズが小さく、速度も速いらしいといろんなサイトに書いてある。例えば、Stackoverflowのこの記事を見ると、Gobの方がJSONよりデータサイズも半分以下で、速度も倍以上速い。すごいね、もうJSONなんか使ってる場合やないね。

単体の場合はJSONの方が小さい!

ちょうどKafkaでデータをやり取りするためのコードを書きかけていたので、シリアライゼーションのところで使ってみようと思い、Gobを試してみると、なぜかデータサイズがJSONより小さくならない、ていうか大きくなる。例えば次のようなコードを実行すると、JSONが24バイト、Gobが50バイトとJSONの方が小さくなる!?

package main

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"fmt"

	"github.com/tada3/gob-test2/domain"
)

func main() {
	p := domain.Person{
		Name: "Taro",
		Id:   123,
	}

	checkSize(p)
}

func checkSize(p domain.Person) {
	b, err := serializeJSON(p)
	if err != nil {
		panic(err)
	}
	fmt.Println("JSON:", len(b))

	b1, err := serializeGob(p)
	if err != nil {
		panic(err)
	}
	fmt.Println("Gob:", len(b1))
}

func serializeJSON(p domain.Person) ([]byte, error) {
	b, err := json.Marshal(p)
	if err != nil {
		return nil, err
	}
	return b, nil
}

func serializeGob(p domain.Person) ([]byte, error) {
	buf := &bytes.Buffer{}
	enc := gob.NewEncoder(buf)
	err := enc.Encode(p)
	if err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}
package domain

type Person struct {
	Name string
	Id   int
}

結果はこの通り。

JSON: 24
Gob: 50

スライスなら勝てる?

おかしいな、と思いながら上のStackflowの記事のソースコードを見てみるとシリアライズの対象がstructのスライスであることに気づく。条件を同じにするために、上のテストコードを変更し、要素10のスライスをシリアライズするようにすると、JSONが241バイト、Gobが154バイトと逆転!

package main

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
	"fmt"
	"strconv"

	"github.com/tada3/gob-test2/domain"
)

func main() {
	ps := make([]domain.Person, 10)
	for i := 0; i < 10; i++ {
		ps[i].Name = "Taro" + strconv.Itoa(i)
		ps[i].Id = i
	}

	checkSize(ps)
}

func checkSize(ps []domain.Person) {
	// 単体の場合と同様
}

func serializeJSON(ps []domain.Person) ([]byte, error) {
	// 単体の場合と同様
}

func serializeGob(ps []domain.Person) ([]byte, error) {
	// 単体の場合と同様
}
JSON: 241
Gob: 154

型情報のサイズ

どうやら、同じstructを繰り返しシリアライズすると効率が良くなるように見える。今一度GobのGoDocを良く見てみるとEncode()のところに

Encode transmits the data item represented by the empty interface value, guaranteeing that all necessary type
information has been transmitted first.

と書いてある。Encode()は初回はデータ自体のシリアライズ結果だけではなく、型情報もシリアライズデータ(シリアライズされたデータ)に含める必要があるということだ。つまり、Gobの方がJSONより小さいというのは型情報を含まない、データ本体の部分に関しては常に正しいが、型情報のサイズも含めてしまうと成り立たないこともある。(一個目の例がそう。) 一方、型情報が必要なのは初回だけなので、繰り返し同じデータをシリアライズすると型情報のコストが薄まっていき、一定数を超えるとJSONよりも小さくなる。

結論

で、結局サイズだけで考えた場合にGobとJSONとどっちが良いのか?答えはEncoder/Decoderの使い方によると思われる。型情報のコストをなくすには、

  1. 同一のEncoder/Decoderのインスタンスのペアが、
  2. 一定数のパターンのスキーマに従うデータ構造を繰り返しシリアライズ/でシリアライズする

ということが必要となる。通常のアプリケーションだと扱うデータ構造のパターンは決まっているので、ほとんどの場合2は成り立つと思われるので1が成り立つがどうかが問題となる。クライアントアプリとサーバアプリでセッションを張ってデータをやりとりするような場合だと問題ないが、Webアプリケーションの場合は普通に作るとリクエストごとにEncoder/Decoderのインスタンスを作成するような形になってしまう。こういうケースは素直にJSONを使えば良いと思う。が、とにかくGobでデータサイズを減らしたい場合は、排他制御に気をつけつつ単一のEncoder/Decoderを使い回すか、コネクションプールみたいなやり方で一定数のEncoder/Decoderインスタンスを再利用する等の工夫が必要になってくる。