blog.takurinton.dev

easyjsonを使ってみた

2020-10-21

こんにちは

どうも、 です。
この記事は随分前のインターン期間中に自分のために書いた記事を転載してます。

GoでJSON使う時ってだいぶめんどくさいんですよね。まあ型による安心感がバケモンなのでやった方がいいんですけど。 GoでJSONを捌く時はstructを使用します。
クラスとかはないのでこれでいきます。 

Unmarshal

早速やってみます。 例えばこんな感じのjsonがくるとします。

var req string
req = `[
            {"name": "takurinton", "age": 20, "favorite": ["runnning", "baseball"]}, 
            {"name": "ryota", "age": 16, "favorite": ["fishing", "baseball"]}, 
            {"name": "hoge", "age": 26, "favorite": ["programming"]}, 
            {"name": "fuga", "age": 10, "favorite": ["study", "programming"]}
        ]`

それをGoで扱える形に変換したい時はこんな感じで書いてあげます。

package main

type Human struct {
    Name string 
    Age int 
    Favorite []string
}

func main() {
    var req string
    req = `[
            {"name": "takurinton", "age": 20, "favorite": ["runnning", "baseball"]}, 
            {"name": "ryota", "age": 16, "favorite": ["fishing", "baseball"]}, 
            {"name": "hoge", "age": 26, "favorite": ["programming"]}, 
            {"name": "fuga", "age": 10, "favorite": ["study", "programming"]}
        ]`

    bytes := []byte(req) // byte型に変換
    var human []Human
    if err := json.Unmarshal(bytes, &human); err != nil {
        log.Fatal(err)
    }
    for _, h := range human {
        fmt.Printf("name: %s, age: %d, favorite: %v\n ", h.Name, h.Age, h.Favorite)
    }
}
name: takurinton, age: 20, favorite: [runnning baseball]
name: ryota, age: 16, favorite: [fishing baseball]
name: hoge, age: 26, favorite: [programming]
name: fuga, age: 10, favorite: [study programming]

Goには Unmarshal というjsonをサポートしてくれる関数が準備されていて、これを利用することでよしなに変換してくれるわけです。 こやつはこんな感じの構造をしてます。第一引数はbyteのスライス、第二引数はインターフェイスを渡してあげます。上のプログラムでもしっかり値を渡すことができています。

func Unmarshal(data []byte, v interface{}) error

Marshal

逆もできます。

func main() {
    var req string
    req = `[
            {"name": "takurinton", "age": 20, "favorite": ["runnning", "baseball"]}, 
            {"name": "ryota", "age": 16, "favorite": ["fishing", "baseball"]}, 
            {"name": "hoge", "age": 26, "favorite": ["programming"]}, 
            {"name": "fuga", "age": 10, "favorite": ["study", "programming"]}
        ]`

    bytes := []byte(req) // byte型に変換
    var human []Human
    if err := json.Unmarshal(bytes, &human); err != nil {
        log.Fatal(err)
    }

    h, err := json.Marshal(human)
    if err != nil {
        log.Fatal(err)
    }
    res := string(h) // stringに変換
    fmt.Println(res)
}
[{"Name":"takurinton","Age":20,"Favorite":["runnning","baseball"]},{"Name":"ryota","Age":16,"Favorite":["fishing","baseball"]},{"Name":"hoge","Age":26,"Favorite":["programming"]},{"Name":"fuga","Age":10,"Favorite":["study","programming"]}]

Marshal の中身はこんな感じです。 値はさっきUnmarshalで使ったものをそのまま使用しました。 戻り値が[]byteとのことなのでいい感じにするためにstringに変換しました。

func Marshal(v interface{}) ([]byte, error)

いい感じに変換されました。

これどうやら遅いらしい

らしいです。

easyjson

そこで出てくるのが、 easyjson というやつです。詳しくは自分で調べてください。 go getで持ってきます。

go get -u github.com/mailru/easyjson/...

これを使うと構造体ごとにコードを自動生成してReflectionなしで高速で先ほどのMarshalやUnmarshalができるようになります。

まずは適当なファイルを作成します。今回はeasy.goというファイルを作成しました。

package main

type Human struct {
    Name     string
    Age      int
    Favorite []string
}

これを作成したら、ターミナルで以下のコマンドを叩きます。

easyjson -all easy.go

そうすると、同じディレクトリの中に新しくeasy_easyjson.goというファイルが自動で作られます。 中身はこんな感じになってます。

// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.

package main

import (
    json "encoding/json"
    easyjson "github.com/mailru/easyjson"
    jlexer "github.com/mailru/easyjson/jlexer"
    jwriter "github.com/mailru/easyjson/jwriter"
)

// suppress unused package warning
var (
    _ *json.RawMessage
    _ *jlexer.Lexer
    _ *jwriter.Writer
    _ easyjson.Marshaler
)

func easyjson97766e5aDecodeJsonPracticeEasy(in *jlexer.Lexer, out *Human) {
    isTopLevel := in.IsStart()
    if in.IsNull() {
        if isTopLevel {
            in.Consumed()
        }
        in.Skip()
        return
    }
    in.Delim('{')
    for !in.IsDelim('}') {
        key := in.UnsafeFieldName(false)
        in.WantColon()
        if in.IsNull() {
            in.Skip()
            in.WantComma()
            continue
        }
        switch key {
        case "Name":
            out.Name = string(in.String())
        case "Age":
            out.Age = int(in.Int())
        case "Favorite":
            if in.IsNull() {
                in.Skip()
                out.Favorite = nil
            } else {
                in.Delim('[')
                if out.Favorite == nil {
                    if !in.IsDelim(']') {
                        out.Favorite = make([]string, 0, 4)
                    } else {
                        out.Favorite = []string{}
                    }
                } else {
                    out.Favorite = (out.Favorite)[:0]
                }
                for !in.IsDelim(']') {
                    var v1 string
                    v1 = string(in.String())
                    out.Favorite = append(out.Favorite, v1)
                    in.WantComma()
                }
                in.Delim(']')
            }
        default:
            in.SkipRecursive()
        }
        in.WantComma()
    }
    in.Delim('}')
    if isTopLevel {
        in.Consumed()
    }
}
func easyjson97766e5aEncodeJsonPracticeEasy(out *jwriter.Writer, in Human) {
    out.RawByte('{')
    first := true
    _ = first
    {
        const prefix string = ",\"Name\":"
        out.RawString(prefix[1:])
        out.String(string(in.Name))
    }
    {
        const prefix string = ",\"Age\":"
        out.RawString(prefix)
        out.Int(int(in.Age))
    }
    {
        const prefix string = ",\"Favorite\":"
        out.RawString(prefix)
        if in.Favorite == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
            out.RawString("null")
        } else {
            out.RawByte('[')
            for v2, v3 := range in.Favorite {
                if v2 > 0 {
                    out.RawByte(',')
                }
                out.String(string(v3))
            }
            out.RawByte(']')
        }
    }
    out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v Human) MarshalJSON() ([]byte, error) {
    w := jwriter.Writer{}
    easyjson97766e5aEncodeJsonPracticeEasy(&w, v)
    return w.Buffer.BuildBytes(), w.Error
}

// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Human) MarshalEasyJSON(w *jwriter.Writer) {
    easyjson97766e5aEncodeJsonPracticeEasy(w, v)
}

// UnmarshalJSON supports json.Unmarshaler interface
func (v *Human) UnmarshalJSON(data []byte) error {
    r := jlexer.Lexer{Data: data}
    easyjson97766e5aDecodeJsonPracticeEasy(&r, v)
    return r.Error()
}

// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Human) UnmarshalEasyJSON(l *jlexer.Lexer) {
    easyjson97766e5aDecodeJsonPracticeEasy(l, v)
}

なんとも便利な〜 これは先ほどのMarshalやUnmarshalと同じように使うことができます。

まずはMarshalから

package main

import (
    "fmt"
    "json_practice/easy"
    "log"

    "github.com/mailru/easyjson"
)

func main() {
    req := Human{
        Name: "takurinton",
        Age:  20,
        Favorite: []string{
            "running",
            "baseball",
        },
    }

    h, err := easyjson.Marshal(req)
    if err != nil {
        log.Fatal(err)
    }
    res := string(h)
    fmt.Println(res)
}
{"Name":"takurinton","Age":20,"Favorite":["running","baseball"]}

こんな感じでうまく変換することができます。同じように使用できるのはいいですねえ。easy.goを構造体ではなくスライスの中に構造体入れるみたいな感じにしてあげればそれもまたいい感じに変換してくれます。

Unmarshalも上と同様に同じ値を使って実装してみたいと思います。

package main

import (
    "fmt"
    "json_practice/easy"
    "log"

    "github.com/mailru/easyjson"
)

func main() {
    req := Human{
        Name: "takurinton",
        Age:  20,
        Favorite: []string{
            "running",
            "baseball",
        },
    }

    h, err := easyjson.Marshal(req)
    if err != nil {
        log.Fatal(err)
    }
    human := Human{}
    if err := easyjson.Unmarshal(h, &human); err != nil {
        log.Fatal(err)
    }

    fmt.Println(human)
}
{takurinton 20 [running baseball]}

こんな感じで出力されます。 結構簡単に実装できますね。

まとめ

これ実はインターンで実装して、めちゃくちゃタイム速くなったんですよね。
標準パッケージのencoding/jsonよりもだいぶ速さが出る上に、安定して高速を出すことができます。
しかし、上で実装してるとわかりますが、特定のstructに対してしか効果を発揮しませんので汎用性には欠けます。使い所を間違えると大変なことになりそうなのできっちり締めるところは締めるみたいな時(?)には使えそう。。。
てな感じで今日は転載記事なのでほぼ文章書いてませんが以上です♪(´ε` )