LoginSignup
7
2

More than 5 years have passed since last update.

json.Marshalの中でMarshalJSONを呼ぶのは、バッドプラクティスかもしれない

Last updated at Posted at 2017-12-23

きっかけ

generic というジェネリックスもどきな独自型をGoで作っているのですが、現在大幅な変更を行っており、行いました。そこでベンチマークを取った際にMarshalJSONとUnmarshalJSONのパフォーマンスが悪いことがわかりました。

実装の違い

ベンチマーク

変更前がversion 1.0.0
変更後がversion 2.0.0

version requests /op B/op allocs/op
1.0.0 5000000 240 ns 185 3
2.0.0 200000000 6.69 ns 0 0

変更前(version 1.0.0)

よく紹介されるMarshalJSONのような感じで書いていました。

// Bool is generic boolean type structure
type Bool struct {
    ValidFlag
    bool bool
}

// MarshalJSON implements the json.Marshaler interface.
func (v Bool) MarshalJSON() ([]byte, error) {
    if !v.Valid() {
        return json.Marshal(nil)
    }
    return json.Marshal(v.Bool)
}

変更後(version 2.0.0)

※ Bool.Boolが、Bool.boolがになったのはこの記事の件とは関係ないので気にしないでください

MarshalJSONでどのようにjsonにencodeしたいのか、はっきりしているので、json.Marshalで処理せずに自分で処理を書いて対応しています。

// Bool is generic boolean type structure
type Bool struct {
    ValidFlag
    bool bool
}

// MarshalJSON implements the json.Marshaler interface.
func (v Bool) MarshalJSON() ([]byte, error) {
    if !v.Valid() {
        return nullBytes, nil
    }
    if v.bool {
        return []byte("true"), nil
    }
    return []byte("false"), nil
}

なぜパフォーマンスが改善したのか?

JSONのエンコードの処理はkey/valueが[]byteで管理できるようになるまで、interfaceもしくは、reflect.Valueで管理されることになります。
必要に応じて型判定を繰り返し、json.Marshalerを継承していることが判定されると、marshalerEncoderという関数が呼ばれ、その中で実装したMarshalJSONを呼ばれる事になります。
この中で、json.Marshalを使った場合、今まで行ってきた処理の後続処理ではなく、上記に書いてきたことを1から行い、[]byteに変換できるか、nilもしくはGoで定義済みの型に変換できるになるまで、これを繰り返すことになります。

TODO: encoding/jsonの実装を後で詳しく書く。

参考にしたpackage

goのtimeは様々なMarshaler/Unmarshalerが用意されており、コメントも充実していたのでとても参考になりました。
https://golang.org/src/time/time.go

UnmarshalJSONは対応しないの?

UnmarhshalJSONも対応を考えたのですが、以下のことを理由に対応を見送りました。

  • jsonかどうかの判定
  • 入力値/型は不明確

jsonかどうかの判定

これが一番のボトルネックだと思います。

Go 1.9 には、encoding/jsonValidというJSONエンコーディングが正しいのか判定する関数がありますが、1.8以下ではこれがありません。
それまで、encoding/jsonにこういった処理がまったくなかったかと言うとそうではなく、privateな関数になってました。
そのため 1.8未満でJSONエンコーディングの判定をしようとすると、encoding/jsonにあった処理と同等な処理を自前で実装する事になります。
OSSで公開していない場合ならそれでもいいのかもしれませんが、そういった都合上で1.8以下を切るといった判定でできないので、json.Unmarshalに任せることにしました。

入力値/型は不明確

MarshalJSONはstructureが保持している値を変換してあげるだけなので、何をどう変換すればいいかが明確です。
それに対し、UnmarhshalJSONはどんな値がどんな型で渡ってくるかがわかりません。
なので、こちらが想定している値/型が渡ってきていることを判定する必要がありますが、自分が作っていたものが様々な型の値を許容していたので、メンテナンスコストを考えて保留としました。

型や値がある程度絞られるようだったら、UnmarhshalJSONでもjson.Unmarshalした方が良いかと思います。

7
2
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
7
2