きちんとやるnet/http


2 min read
きちんとやる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を使ってタイムアウトの指定をしたい、などより複雑になる可能性もあります。
一見単純なリクエストですが、きちんと気を遣っていきたいですね。


gitにもaliasの指定ができる件
Previous article

gitにもaliasの指定ができる件

tl;dr .gitconfigにもaliasの登録ができる [alias]ブロックにaliasを登録する tagsで単数・複数の悩みを解消する discardで変更を取り消す unstageでaddを取り消す uncommitでcommitを取り消す ignoreで.gitignoreを生成する git aliases この記事は今すぐalias登録すべきワンライナー by ゆめみ① Advent Calendar 2018の6日目の穴埋め記事です。 こちらのアドベントカレンダーは今すぐalias登録べきワンライナーということで、みなさん.bashrcや.zshrcのaliasについて記事を書いてらっしゃいますが、実は.

jessevdk/go-assetsでファイルを埋め込む
Next article

jessevdk/go-assetsでファイルを埋め込む

Go言語の素敵なところの一つとして、最終的な成果物を1バイナリに収めることができる、という点にあると思う。結果として、非常に簡単にコマンドラインツールなどを配布することができる。 しかし、例えばコード生成を行うようなツールでテンプレートファイルを別途持っているような場合や、アプリケーション中で使う画像などを含む場合など、Goのソースコード以外のファイルを必要とする場合、全てを1ファイルで、とはいかない。 そのような場合に便利なのがjessevdk/go-assetsである。以前は多くの人がgo-bindataを使用していたと思われるが、作者がやめてしまったため、使えなくなってしまった。代替としてこれが便利。 jessevdk/go-assetsを使用するには、まずjessevdk/go-assets-builderを使用する。これは、指定したファイルをGoのソースコードに埋め込んで、それらを扱うためのAssetsというオブジェクトを作成してくれるツールである。 インストールは簡単で、


GO TOP

🎉 You've successfully subscribed to something tech.!
OK