Go言語でJSONのデコード処理を検証してみた
GOでJSONをデコードする際の方法
Goで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として定義されたJSONをjson.Unmarshalでデコード
- ポイントは、デコード結果を受け取るために構造体を定義している点
- 対象のJSONデータの構造を確認しつつ、実装時に構造体の定義を決める必要がある
複雑な構造の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) }
デコード結果格納までのコードの見通しをよくする
- 上記の例では、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アプリが公開されている
interfaceを利用して局所的に参照する
- 必要なオブジェクトをピンポイントに取り出す
- 各オブジェクトへのアクセスには型アサートを利用する
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
- https://qiita.com/msh5/items/dc524e38073ed8e3831b
- https://stackoverflow.com/questions/21197239/decoding-json-in-golang-using-json-unmarshal-vs-json-newdecoder-decode
- http://kudohamu.hatenablog.com/entry/2014/11/05/165133
- https://qiita.com/nayuneko/items/2ec20ba69804e8bf7ca3
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.