Edited at

Go言語でJSONに泣かないためのコーディングパターン

More than 1 year has passed since last update.

REST API 全盛の昨今、Go で JSON を扱うケースも多いとおもうのですが、言語としての静的型付けの性質が JSON とマッチせず、愚直にコーディングすると扱いに非常に骨が折れます。

私は Go 言語を始めてまだ数週間ですが、兼ねてより JSON を扱うのが煩わしくて、あやうく Go 言語を窓から捨てて、動的片付けの世界に戻ろうとしていました。

本稿では、私が最終的に行きついた「これだ!」と思うアプローチを2つ紹介しようと思います。:point_up:

1つ目は、正攻法ですが Go の構文仕様の中で工夫することで、実装を簡素化しようというアプローチです。

もうひとつは、正攻法とは違ったアプローチで、JSON の特定のオブジェクトのみを参照するユースケースにおいては、より実装を簡素化できるアプローチです。


Go 言語での JSON 処理

本稿は Go の初学者を対象としているので、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
}

b として定義された JSON を、json.Unmarshall でデコードしています。

ポイントは、デコード結果を受け取るために構造体を定義している点です。:point_up:

デコード結果を受け取る構造体は JSON と同じ構造をしている必要があるので、対象の JSON データの構造を確認しつつ、実装時に構造体の定義を決める必要があります。


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

簡単な JSON を扱った例だけだと何の問題もないように見えると思いますが、対象とする JSON データの構造が複雑な場合、具体的には多重の入れ子を含むケースだと実装がだんだん大変になってきます。

例として、Elasticsearch の REST API を叩くコードを使いたいと思います。

Elasticsearch の検索 API は処理結果として複雑な JSON を返します。


検索APIの出力例

{

"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
}
}
]
}
}

この出力を Go でデコードする処理を、愚直にコーディングしてみます。

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 の出力結果にあった構造体群を定義する必要がありますし、リクエストボディに JSON を渡す必要がある場合は、エンコードのための構造体群も定義する必要があります。

こうして、JSON が出てくるたびに無知な Go プログラマは憂鬱な気持ちになれるわけです。:smiling_imp:

次の節からは、こういった複雑な構造を持った JSON の処理を効率的に実装するための方法を、コーディングパターンとして紹介します。

アプローチの違う2つの方法がありますので、順に説明します。


パターン1「デコード結果格納までのコードの見通しをよくする」

前の節で例示したコードですが、実は 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 の出力形式に見た目が近くなり、かつコード量も減ったことで、だいぶわかりやすくなりました。

さらに、この構造体はコード内で1か所しか使用しないので、型名付きでグローバルに定義する必要もありません。

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 {
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 {
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)
}
println(result.Hits.Total)
}

初めと比較すると、非常にわかりやすくなったと思います。


発展的な実装テクニック

前の節の例では、出力される JSON の形式を完全に構造体でカバーして、まるっと全ての値をデコードしました。

実は必ずしもそうする必要はなく、参照が必要なオブジェクトだけを構造体の定義でおさえるようにしても、デコード処理に支障は出ません。

構造体の定義に存在するオブジェクトの値のみが格納され、それ以外のオブジェクトは無視されます。:point_up:

例えば、ヒットしたドキュメントのリストのみを参照したい場合は以下のように実装することが出来ます。

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 データに含まれるオブジェクトには、エラー情報のように「もしかしたら返却されるかもしれない」オブジェクトというのもあると思います。

こういったときは対応するフィールドをポインタ型で用意しましょう。

encoding/json の仕様として、ポインタ型として定義されたフィールドは、対応するオブジェクトが JSON 上に存在しない場合はそのままで、存在する場合は実体を生成した上で値を格納するという挙動をします。:point_up:

よって、返却されるかもしれないし、されないかもしれないオブジェクトに対応するフィールドは、ポインタ型として定義することで、必要となるまでメモリ確保を遅延することが出来ます。


構造体定義の自動生成

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

JSON-to-Go

キャプチャ.JPG

JSON と書かれた左側のペインに JSON データをペーストすると、それに対応する構造体が右側のペインにインタラクティブに反映されて表示されます。


パターン2「interface を利用して局所的に参照する」

もうひとつコーディングパターンを紹介します。

パターン1のアプローチは、出力された JSON 全体をまるっとデコードして取り出したいときには便利ですが、JSON 上のいくつかのオブジェクトだけに用がある場合には、仰々しい印象がありますね。

そこで、必要なオブジェクトをピンポイントに取り出すアプローチを紹介します。

パターン1では、結果として返される JSON データと対応する構造をもった構造体を定義して、これに結果を格納するというアプローチをとりましたが、パターン2では構造体の代わりに interface 型として結果の格納先を定義します。

interface 型を利用することで、パターン1で煩雑だった構造体定義を省略することができます。

各オブジェクトへのアクセスには型アサートを利用しますが、注意点として数値型はすべて float64 型として変換されますので、仮に JSON 上で整数値が入っていても、それを int 型として変換しようとするとエラーになりますのでお気をつけください。

※ JSON データ上での型と Go 側の型の対応はドキュメントで定義されています。(https://golang.org/pkg/encoding/json/#Unmarshal

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)
}

パターン2を使って、Elasticsearch の検索 API の返却するデータをデコードした例です。

JSON データ内に検索結果として格納されている、ヒット件数の値を取り出しています。

先ほど触れたように、数値型はすべて float64 として格納されているので、取得後に int にキャストするという操作を行っています。


json.Number を介した変換規則の有効化

先ほど注意事項として、数値型の変換規則について述べさせて頂きました。

この挙動は、中に入っている値が整数値か浮動小数点値が入っているかわからないケースを想定したものだと思うのでが、中に入っている値が整数値と決まっているケースでは、コードが冗長になってしまいます。

そこでですが、encoding/json はオプションとして別の変換規則を用意していて、それは数値型を float64 ではなく json.Number という中間形式に変換するという方式です。

json.Number には Int64, Float64, String の3つのメソッドが用意されていて、値をどの型に変換して取得するかを呼び出し側で選択することが出来ます。

このオプションの変換規則を有効にするには、作成した Decoder オブジェクトの UseNumber() メソッドを呼び出します。

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)
decoder.UseNumber()
if err := decoder.Decode(&result); err != nil {
panic(err)
}
n, _ := result.(map[string]interface{})["hits"].(map[string]interface{})["total"].(json.Number).Int64()
println(n)
// 359
}

オプショナルな変換規則を有効にしたことによって冗長性はなくなりますが、json.Number への変換が挟まるのでコード量は減りませんね。

むしろ、UseNumber() を呼び出す必要がある分、コード量という点では増えています。

どちらの変換規則を選ぶかは好みの問題でしょう。


Jason ライブラリの紹介

最後に、Jason という Github 上で個人開発されているパッケージを紹介します。

Go の JSON ライブラリの中では比較的なメジャーなもので、 Qiita でも hironobu_s さんが紹介されています。

Go言語でルーズにJSONを扱えるライブラリJasonが便利だった

Jason のソースをみると分かりますが、先ほど述べたオプショナルな変換規則を用いたアプローチをラップしています。

オプショナルな変換規則を有効にすると、コード量が増えるというデメリットが発生しますが、Jason ライブラリを導入することでコード量も削減することが出来ますし、コードの見通しも良くなります。

また、入れ子のオブジェクトを辿るためのインタフェイスも提供されていて、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)
}


エンコード処理に残る煩雑性

ここまでデコード処理に関して、コードの削減や冗長性の排除のためのアプローチを紹介しました。

ただし、エンコード処理に関しては、私の知る限り愚直に記述する以外の方法はありません。

例えば、パターン1として紹介した構造体の定義を簡略化するテクニックですが、入れ子の構造体の初期化をシンプルに記述することが、Go 言語の言語仕様上できませんので、これをエンコード処理に適用することが出来ません。

// こういう記述は出来ても・・

document := struct {
Size int `json:"size"`
}{
Size: 100,
}

// こういう記述は出来ない
document := struct {
Size int `json:"size"`
Query struct {
Match struct {
Haystack string `json:"haystack"`
} `json:"match"`
} `json:"query"`
}{
Size: 100,
Query: { // この辺りから構文エラー
Match: {
Haystack: "needle"
}
}
}

パターン2に関しても、interface 型として定義した変数に型アサートを使って参照する操作は、デコード処理においてのみ有効なテクニックであり、Jason パッケージもエンコード処理に関しては、最低限の機能しか提供しておりません。


まとめ

JSON デコード処理のコーディングパターンとして、2つのアプローチを紹介しました。

エンコード処理においては課題が残りますが、今回紹介したコーディングパターンを利用することで、高い生産性が得られることと思います。:sunny: