emahiro/b.log

勉強記録と書評とたまに長めの呟きを書きます

これからはgoのstreamを使っていこうと思った話

これからgoでjson形式のデータをstructにmappingする際にはstreamを使うことを決意した話です。

経緯

下記のエントリーに触発されました。

christina04.hatenablog.com

goでjsonをstructにmappingすることをはじめ、 io.Reader のデータを扱うときは一度 []byte 型に変換してから諸々の処理を行うというのが一般的な実装方法です。

一般的なjsonのmapping

APIのレスポンスなど、goにおいて io.Reader 型を受け取り、[]byte に変換してからjsonのデータ構造をオブジェクトにmappingするときの一般的なコードは下のような形式になるかと思います。

client := http.Client{}
resp, err := client.Get(url)
if err != nil {
  fmt.Printf("Error: get error. err: %v", err)
  return nil, err
}

body := resp.Body()
defer body.Close()

b, err := ioutil.ReadAll(body)
if err != nil {
  panic(err)
}

var data SomeData
if err := json.Unmarshal(b, &data); err != nil {
  panic(err)
}

一般的な上記のコードですが、byte型に変換する上で上述のエントリに記載されているようなメモリ効率が悪い点に加えて、

  • httpリクエストのとき
  • []byteに変換するとき
  • jsonをDecodeするとき

の計3回エラーハンドリングの処理を書かなければならず、コードがその分長くなるのが個人的にはちょっと冗長になっていると感じます。

streamを使う

[]byte 型に変換して json.Unmarshal する方法を使わずstreamを利用したコードは以下

client := http.Client{}
resp, err := client.Get(url)
if err != nil {
  fmt.Printf("Error: get error. err: %v", err)
  return nil, err
}

defer resp.Body.Close()
var data SomeData
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
  panic(err)
}

[]byte型に変換せずに json.NewDecoder(io.Reader) にそのまま io.Reader型のbodyを渡して、decodeします。

streamを利用すると、 []byte型に変換する箇所が丸々なくなるので、ここでエラーハンドリングの回数が一回減って、コードも見通しがよくなります。

cf. エントリ内で参照されている

stackoverflow.com

の中で言及されていますが、

So a better rule of thumb is this:

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.

For the case of reading from an HTTP request, I'd pick json.Decoder since you're obviously reading from a stream.

  1. 単一のio.Readerからデータを取得した場合か単一streamにおいて複数の値をDecodeする場合 -> json.Decoderを使うべき
  2. すでにmemoryにjsonのデータを保持している場合 -> json.Unmarshal を使うべき

とされています。
APIからのレスポンスを取得するようなユースケースにおいては最初からmemoryにjsonのデータが保持されているわけではないので、 json.Decoder の方が適しているということですね。

実際に書き換えてみた

以前作った下記のリポジトリxmlをDecodeする箇所をstreamで書き換えてみました。

github.com

https://github.com/emahiro/golang-hatena-client/commit/c1ffbc8bed30a2f526d19a616dc01d360889c78b のコミットで書き換えてます。

まとめ

これからは、io.Reader をinterfaceとして持つ io.ReadCloser 型のBodyなど、streamを使える場合は積極的にstreamのまま使っていこうと思います。
また、io.Reader 型を生成して、使い回すというテクニックも覚えていこうと思います。

例えば....

  • zip.NewReadergzip.NewReader のような NewReader のインターフェースを持つ場合は積極的に、io.Reader型に変換するなど