を返します.GOにおけるJSON処理


あなたが私のようで、バックエンド開発を学びたいならば、あなたはおそらく1点でJSON処理に遭遇しました.JSONはフロントエンドとバックエンドの間でデータを転送するための非常に一般的な形式です.それは近代的なWeb開発のような重要な機能ですので、GOencoding/json パッケージ.
問題は、一つのやり方しかないということです.あなたが過去にいくつかのチュートリアルを見たならば、あなたはJSONを扱うために異なる機能を使用している人々に気がつきました.一部の人々の使用Marshal and Unmarshal , 他使用中Encode and Decode . 何を使うべきですか.どちらが良いですか.このブログ記事では、2つのアプローチの違いを説明しようとします.楽しむ!

しかし、まず、2つのアプローチをお見せしましょう。


JSONを読み書きするには二つの方法があります.このコードスニペットは、2つの方法を使用する方法を理解するのに役立ちます.最初にMarshal and Unmarshal :
func PrettyPrint(v interface{}) (err error) {
    b, err := json.MarshalIndent(v, "", "\t")
    if err == nil {
        fmt.Println(string(b))
    }
    return err
}

func TryMarshal() error {
    data := map[string]interface{}{
        "1": "one",
        "2": "two",
        "3": "three",
    }
    result, err := json.Marshal(&data)
    if err != nil {
        return err
    }

    err = PrettyPrint(result)
    if err != nil {
        return err
    }

    return nil
}

func TryUnmarshal() error {
    myFile, err := os.Open("test.csv")
    if err != nil {
        return err
    }
    defer myFile.Close()

    data, err := io.ReadAll(myFile)
    if err != nil {
        return err
    }

    var result map[string]interface{}
    json.Unmarshal([]byte(data), &result)

    err = PrettyPrint(result)
    if err != nil {
        return err
    }

    return nil
}
インTryMarshal , 私はmap[string]interface{} データを保持する.私はそれをパスしたMarshal .
インTryUnmarshal , ファイルを読んでバイトスライスに変換しますdata . あれdata が渡されるUnmarshal , を返します.map[string]interface{} .PrettyPrint ちょうどそれが素敵に見えるように出力をフォーマットします.
では、見てみましょうEncoder.Encode and Decoder.Decode .
func TryEncode() error {
    data := map[string]interface{}{
        "1": "one",
        "2": "two",
        "3": "three",
    }
    err := json.NewEncoder(os.Stdout).Encode(&data)
    if err != nil {
        return err
    }

    return nil
}

func TryDecode(path string) error {
    myFile, err := os.Open(path)
    if err != nil {
        return err
    }
    defer myFile.Close()

    var result map[string]interface{}
    json.NewDecoder(myFile).Decode(&result)

    return nil
}
コードは前の例とかなり似ています.TryEncode 似ているTryMarhsal and TryDecode 似ているTryUnmarshal . ここだけの違いはEncode and Decode メソッドはEncoder and Decoder 種類NewEncoder を取り込むio.Writer インターフェースとEncoder 種類NewDecoder を取り込むio.Reader インターフェースとDecoder 種類この例では、os.Stdout for NewEncoder and myFileos.File 種類NewDecoder .
これらの関数の使い方を知っているので、フードの下で2つのアプローチがどのように異なるかに飛び込むことができます.

marshal ()およびunmarshal ()


の実装を見てみましょう.
func Marshal(v any) ([]byte, error) {
    e := newEncodeState()

    err := e.marshal(v, encOpts{escapeHTML: true})
    if err != nil {
        return nil, err
    }
    buf := append([]byte(nil), e.Bytes()...)

    encodeStatePool.Put(e)

    return buf, nil
}

func Unmarshal(data []byte, v any) error {
    var d decodeState
    err := checkValid(data, &d.scan)
    if err != nil {
        return err
    }

    d.init(data)
    return d.unmarshal(v)
}
ここで知る必要があるのは、
  • Marshal 任意の値をとるany ラッパアラウンドinterface{} ) バイトスライスに変換します.
  • Unmarshal バイトスライスを取り、それを解析し、結果をv .
  • また、どのように見てみましょうMarshal すべてのバイトをバイトスライスに格納するbuf . これはMarshal すべてのデータをメモリに保持する必要があります.これはかなりのRAM集約することができます.Unmarshal 入力としてバイト全体のスライスを取るので、同様の問題があります.

    newEncoding ()encode ()およびnewdecode ()decode ()


    encoding ()およびdecode ()のコードを示します:
    func (enc *Encoder) Encode(v any) error {
        if enc.err != nil {
            return enc.err
        }
        e := newEncodeState()
        err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML})
        if err != nil {
            return err
        }
    
        e.WriteByte('\n')
    
        b := e.Bytes()
        if enc.indentPrefix != "" || enc.indentValue != "" {
            if enc.indentBuf == nil {
                enc.indentBuf = new(bytes.Buffer)
            }
            enc.indentBuf.Reset()
            err = Indent(enc.indentBuf, b, enc.indentPrefix, enc.indentValue)
            if err != nil {
                return err
            }
            b = enc.indentBuf.Bytes()
        }
        if _, err = enc.w.Write(b); err != nil {
            enc.err = err
        }
        encodeStatePool.Put(e)
        return err
    }
    
    func (dec *Decoder) Decode(v any) error {
        if dec.err != nil {
            return dec.err
        }
    
        if err := dec.tokenPrepareForDecode(); err != nil {
            return err
        }
    
        if !dec.tokenValueAllowed() {
            return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
        }
    
        n, err := dec.readValue()
        if err != nil {
            return err
        }
        dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
        dec.scanp += n
    
        err = dec.d.unmarshal(v)
    
        dec.tokenValueEnd()
    
        return err
    }
    
    コードはここでは長いですが、これらのことを思い出してください.
  • Encode and Decode メソッドはEncoder and Decoder 人気のインターフェイスの周りのラッパーですio.Writer and io.Reader .
  • Encode and Decode ストリームデータを一度にすべてを格納するのではなく.そこからバッファがあるEncode and Decode 書き込みと読み込み、すべてのデータが処理されるまで、これが起こります.
  • ……どちらを使うべきですか。


    良い質問!私は2つのアプローチのパフォーマンスの違いを見て好奇心旺盛だったので、私はテストを書いて、それらをベンチマーク.任意の印刷は、テストのために無効になっていることに注意してください.
    テストのために設計されてUnmarshal and Decode 通常、通常、あなたは巨大なJSONデータを書くことができません.一方、サーバーからの巨大なJSONデータを受け取ることができます.あなたはまだ同様の結果を期待することができますMarshal and Encode なぜなら、彼らは基本的にパートナーの関数とは逆だからです.
    以下にテストコードを示します.
    func BenchmarkTryUnmarshal(b *testing.B) {
        for i := 0; i < b.N; i++ {
            err := TryUnmarshal("file.json")
            if err != nil {
                b.Fatalf("error: %v", err)
            }
        }
    }
    
    func BenchmarkTryDecode(b *testing.B) {
        for i := 0; i < b.N; i++ {
            err := TryDecode("file.json")
            if err != nil {
                b.Fatalf("error: %v", err)
            }
        }
    }
    
    "file.json" は実験変数です.これらはそれぞれの実行のための異なるサイズのJSONファイルになります.最初の5つのJSONファイルはJSONPlaceholder - Free Fake REST API . 最後のJSONファイル(最大のもの)はtest-data/large-file.json at master · json-iterator/test-data · GitHub .
    ここでは、テストのために使用されるアーキテクチャです.
    goos: linux
    goarch: amd64
    pkg: example.com/jsonExperiment
    cpu: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
    
    そしてベンチマークデータです.

    非マーシャル


    JSONファイルサイズ
    ループの実行
    反復回数( ns/op )の時間
    操作ごとに割り当てられたバイト数( B/OP )
    操作あたりの割り当ての数
    7
    12819年
    96463
    56520年
    582年
    13
    7323
    155131
    112736
    1036年
    31
    3748年
    315787
    238736
    1439年
    175
    679年
    1647401
    1300978
    8992
    1252年
    94
    12934180
    10677126
    84603年
    25618
    4
    276607700
    237844490
    1750110

    デコード


    JSONファイルサイズ
    ループの実行
    反復回数( ns/op )の時間
    操作ごとに割り当てられたバイト数( B/OP )
    操作あたりの割り当ての数
    7
    13702年
    88839
    35432
    580
    13
    7882
    149191年
    79312
    1032年
    31
    4260
    280336
    149424
    1433年
    175
    760
    1583824
    965830
    8983年
    1252年
    94
    12613618
    7491156
    84588
    25618
    4
    261644025
    166432316
    1750103年




    ここでいくつかのパターンを見ることができます.
  • Decode 一貫して以下のメモリを使用しますUnmarshal . しかし、これは多くの違いではありません.
  • 他のすべては、それほど異なりません.
  • 大きなJSONファイルを扱っているとき、メモリ使用量は問題になっているようですが、サーバーが巨大なJSONデータを受け取ることはunlinkです.私は最後の例でそれを押していました.
  • 結論


    パフォーマンスの違いはかなり小さいので、どのアプローチを取るべきかを決めるときには、パフォーマンスはmakeまたはbreak factorであるべきではないと思います.考慮するより合理的な方法は、あなたがどのようなデータ形式を使っているかを見ることです.例えば、このスニペットを見てください.
    func Homepage(w http.ResponseWriter, r *http.Request){
        type pageData struct {
            visited time.Time
            message string
        }
        homepageData := pageData{time.Now(), "Welcome!"}
        json.NewEncoder(w).Encode(&homepageData)
    }
    
    func main() {
        http.HandleFunc("/", Homepage)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    これは、APIがGoでどのように見えるかの簡単な例です.へのどんな要請/ エンドポイントは、このコードを実行するトリガHomepage のインスタンスを生成するpageData を使ってエンコードしますNewEncoder(w).Encode(&homepageData) . w 実装io.Writer , だから使用する方が便利ですEncode これはio.Writer . 構造体をバイトスライスに技術的に変換してからMarshal . しかし、なぜ必要なときに余分なステップを取る?
    テイクアウトポイントは、問題になるまでパフォーマンスを心配する必要はありません.代わりに、あなたは、与えられた時間で使用する最も簡単なソリューションを選ぶ必要があります.バイトスライスがあれば使用するMarshal and Unmarshal . があるならばio.Writer またはio.Reader , 用途Encode and Decode .
    読んでくれてありがとう!これは私にとって興味深い話題であり、いくつかの実験をしたかった.コメントの下で私を知っている場合は、これらのタイプのポストのように!このポストを読むことができますMedium and my personal site 同様に.