nasa9084
· 2 min read

きちんとやるnet/http

きちんとやるnet/http

皆さん、net/httpパッケージは使っていますか?
Go言語の標準パッケージであるnet/httpはPythonなどの標準HTTPパッケージに比べ、人間にとっても取り扱いがしやすいため、そのまま使用している方が多いかと思います。
しかし、このnet/httpパッケージ、簡単に使えるように見えて結構落とし穴が多いのです。

1. Response Bodyはクローズする必要がある

次のコードを見てみましょう。

resp, err := http.Get("https://example.com/api")
if err != nil {
    return nil, err
}
var t T
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
    return nil, err
}
return &t, nil

クライアントライブラリなどでよく書きそうな処理ですね。何も問題ないと思いましたか?
公式ドキュメントを見てみましょう。

It is the caller's responsibility to close Body.

Bodyをクローズするのは関数を呼んだ人の責任、とあります。そうです。Response.BodyClose()しなければならないのです。ちゃんとクローズされていない場合、次のリクエストでkeepaliveコネクションの再利用がされず、パフォーマンスの悪化やコネクションリークを起こす可能性があります。

2. Response Bodyを最後まで読む

Response Bodyをきちんとクローズするように修正したコードが次のようなコードです。

resp, err := http.Get("https://example.com/api")
if err != nil {
    return nil, err
}
defer resp.Body.Close()
var t T
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
    return nil, err
}
return &t, nil

deferを使うことできちんとクローズできているはずです。
さて、問題はないでしょうか?いいえ、これだけだとまだkeepaliveコネクションの再利用がされない恐れがあります。

The default HTTP client's Transport may not reuse HTTP/1.x "keep-alive" TCP connections if the Body is not read to completion and closed.

Response Bodyが最後まで読まれていない場合ですね。jsonのデコードの最中にエラーが発生した場合など、最後まで読み込まれていない可能性があります。最後まで読み込む処理を入れましょう。

3. Response Codeをチェックする

Response Bodyを最後まで読み込む処理を加えたのが次のコードです。

resp, err := http.Get("https://example.com/api")
if err != nil {
    return nil, err
}
defer func() {
    defer resp.Body.Close()
    io.Copy(ioutil.Discard, resp.Body)
}
var t T
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
    return nil, err
}
return &t, nil

問題はありますか?はい、きちんとResponse Codeをチェックしましょう。リクエスト時に返ってくるエラーはあくまでリクエスト時のエラーであり、HTTPのステータスコードの確認まではしません。
APIによっては、正常時は200で返すがエラー時(例えば404のとき)は普通にwebページが返ってきてしまう、というAPIもあり得ます。
そんな場合にjsonのDecodeがpanicを起こさないよう、きちんとハンドリングしておきましょう。
また、Response.StatusCodeは単なるintとして定義されています。場合によっては0などのおかしな値が入っていることもあるので、そういった意味でも確認が必要でしょう。

最終コード

最終的には次のようなコードになります。

resp, err := http.Get("https://example.com/api")
if err != nil {
    return nil, err
}
defer func() {
    defer resp.Body.Close()
    io.Copy(ioutil.Discard, resp.Body)
}
if resp.StatusCode < 200 || 299 < resp.StatusCode {
    return nil, errors.New("something error message...")
}
var t T
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
    return nil, err
}
return &t, nil

最初はシンプルなように見えましたが、少し肥大化してしまいました。思っていたよりも注意すべき点があったようです。これに加え、場合によってはcontext.Contextを使ってタイムアウトの指定をしたい、などより複雑になる可能性もあります。
一見単純なリクエストですが、きちんと気を遣っていきたいですね。