web-technical-blog

web開発に関する技術メモ

Go言語でJSONのデコード処理を検証してみた

GOでJSONをデコードする際の方法

GoでJSONを処理する最も一般的な方法

  • Goの標準ライブラリには、JSONを扱うためのパッケージ(encoding/json)が含まれていて
    エンコード(構造体から文字列)およびデコード(文字列から構造体)の両方をサポートしている
package main

import "encoding/json"

type Message struct {
    Name string
    Body string
    Time int64
}

func main() {
    b := []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
    var m Message
    json.Unmarshal(b,&m)
    println(m.Name) // Alice
    println(m.Body) // Hello
    println(m.Time) // 1294706395881547000
}
  • bとして定義されたJSONjson.Unmarshalでデコード
  • ポイントは、デコード結果を受け取るために構造体を定義している点
  • 対象のJSONデータの構造を確認しつつ、実装時に構造体の定義を決める必要がある

複雑な構造のJSON処理で直面する問題

  • JSONデータの構造が複雑な場合、具体的には多重の入れ子を含むケースだと実装がだんだん大変になってくる

  • (例)Elasticsearchの検索APIは処理結果として複雑なJSONを返してくる

{
    "took": 1,
    "timed_out": false,
    "_shards":{
        "total" : 1,
        "successful" : 1,
        "failed" : 0
    },
    "hits":{
        "total" : 1,
        "max_score": 1.3862944,
        "hits" : [
            {
                "_index" : "twitter",
                "_type" : "tweet",
                "_id" : "0",
                "_score": 1.3862944,
                "_source" : {
                    "user" : "kimchy",
                    "message": "trying out Elasticsearch",
                    "date" : "2009-11-15T14:12:12",
                    "likes" : 0
                }
            }
        ]
    }
}

上記JSONデータをデコードする処理を、単純にコーディングすると

package main

import (
    "encoding/json"
    "net/http"
)

type ResultShards struct {
    Total      int `json:"total"`
    Successful int `json:"successful"`
    Failed     int `json:"failed"`
}

type ResultHitSource struct {
    User    string `json:"user"`
    Message string `json:"message"`
    Date    string `json:"date"`
    Likes   int    `json:"likes"`
}

type ResultHit struct {
    Index  string          `json:"_index"`
    Type   string          `json:"_type"`
    Id     string          `json:"_id"`
    Score  float32         `json:"_score"`
    Source ResultHitSource `json:"_source"`
}

type ResultHits struct {
    Total    int         `json:"total"`
    MaxScore float32     `json:"max_score"`
    Hits     []ResultHit `json:"hits"`
}

type Result struct {
    Took     int          `json:"took"`
    TimedOut bool         `json:"timed_out"`
    Shards   ResultShards `json:"_shards"`
    Hits     ResultHits   `json:"hits"`
}

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result Result
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        panic(err)
    }
    println(result.Hits.Total)
}
  • JSON入れ子構造になっているケースでは、結果を受け取る構造体も入れ子に定義する必要がある
  • 実装にかなり苦労する
  • 別のAPIを呼ぶたびにそのAPIの出力結果に応じた構造体群を定義する必要がある

デコード結果格納までのコードの見通しをよくする

  • 上記の例では、Goの平易な言語仕様しか用いていないために、見通しの悪い実装となっているのでリファクタリングする
  • Goは構造体をネスト定義することが可能
  • さらにネスト定義する構造体には型名が必要ないので、これも削除する
package main

import (
    "encoding/json"
    "net/http"
)

type Result struct {
    Took     int  `json:"took"`
    TimedOut bool `json:"timed_out"`
    Shards   struct {
        Total      int `json:"total"`
        Successful int `json:"successful"`
        Failed     int `json:"failed"`
    } `json:"_shards"`
    Hits struct {
        Total    int     `json:"total"`
        MaxScore float32 `json:"max_score"`
        Hits     []struct {
            Index  string  `json:"_index"`
            Type   string  `json:"_type"`
            Id     string  `json:"_id"`
            Score  float32 `json:"_score"`
            Source struct {
                User    string `json:"user"`
                Message string `json:"message"`
                Date    string `json:"date"`
                Likes   int    `json:"likes"`
            } `json:"_source"`
        } `json:"hits"`
    } `json:"hits"`
}

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result Result
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        panic(err)
    }
    println(result.Hits.Total)
}
  • ネストに定義することで、本来のJSONの出力形式に見た目が近くなり、かつコード量も減る

発展的な実装テクニック

  • 出力されるJSONの形式を完全に構造体でカバーする必要がなく、参照が必要なオブジェクトだけを構造体の定義でおさえるようにしても、デコード処理に支障は出ない
  • ヒットしたドキュメントのリストのみを参照した場合は以下のように実装することができる
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result struct {
        Hits struct {
            Hits     []struct {
                Source struct {
                    Title       string `json:"title"`
                    Description string `json:"description"`
                    ImageUrl    string `json:"image_url"`
                    Url         string `json:"detail_url"`
                } `json:"_source"`
            } `json:"hits"`
        } `json:"hits"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        panic(err)
    }
    for _, hit := range result.Hits.Hits {
        println(hit.Source.Title)
        println(hit.Source.Description)
        println(hit.Source.ImageUrl)
        println(hit.Source.Url)
    }
}
  • JSONデータに含まれるオブジェクトには、エラー情報のように「もしかしたら返却されるかもしれない」オブジェクトというものもあるので、こういった場合は対応するフィールドをポインタ型で用意するう

構造体定義の自動生成

  • デコード結果を受け取る構造体の定義は、JSONデータから自動生成するWebアプリが公開されている

json-to-go

interfaceを利用して局所的に参照する

  • 必要なオブジェクトをピンポイントに取り出す
  • 各オブジェクトへのアクセスには型アサートを利用する
    • 注意点としては数値型はすべてfloat64型として変換される
    • JSON上に整数値が入っていても、それをint型として変換しようとするとエラーになる
    • JSONデータ上での型とGo側の型の対応ドキュメント
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result interface{}
    decoder := json.NewDecoder(resp.Body)
    if err := decoder.Decode(&result); err != nil {
        panic(err)
    }
    n, _ := result.(map[string]interface{})["hits"].(map[string]interface{})["total"].(float64)
    println(int(n))
    // 359
    // (int にキャストしなかった場合:+3.590000e+002)
}
  • JSONデータ内に検索結果として格納されている、ヒット件数の値を取り出しています
  • 整数型はすべてfloat64として格納されているので、取得後にintにキャストする操作を行っている

Jasonライブラリ

  • JasonというGitHub上で個人開発されているパッケージ
  • GoのJSONライブラリの中では比較的なメジャーなもの
  • 入れ子のオブジェクトを辿るためのインタフェイスも提供されていて、Jason を利用しない場合、型アサートを繰り返し記述することになりますが、これを簡略化して実装できる
package main

import (
    "github.com/antonholmquist/jason"
    "net/http"
)

func main() {
    resp, err := http.Get("http://127.0.0.1:9200/_search")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    v, err := jason.NewObjectFromReader(resp.Body)
    if err != nil {
        panic(err)
    }

    n, err := v.GetInt64("hits", "total")
    if err != nil {
        panic(err)
    }
    println(n)
}

参考URL

DecoderとUnmarshalの使い分け

  • データがio.Readerストリームからのものである場合、またはデータストリームから複数の値をデコードする必要がある場合は、json.Decoderを使用します。
  • すでにJSONデータがメモリにある場合は、json.Unmarshalを使用します。

Use json.Decoder if your data is coming from an io.Reader stream, or you need to decode multiple values from a stream of data.

Use json.Unmarshal if you already have the JSON data in memory.