APIなどを叩く際に、net/httpパッケージを使用して特定URLにリクエストを投げることが一般的かと思います。リクエストをしたら戻ってくるResponse、特にそのBodyをどんな時にCloseするべきなのか?を正確に知りたくなったので、Response Bodyの安全な取り扱い方法を調べてみました。
他の記事では、内部詳細まで詳しく解説されている記事がありますが、本記事では、僕のようなGo初心者向けにポイントで理解できるような構成でまとめています。
参考にするべきURLも併記するようにしていますので、内部仕様など詳細が気になる方は参考URLも併せてご覧ください。
【結論】Response Bodyを安全に取り扱う2つのポイント
リクエストで得たResponse Bodyを安全に取り扱うために、まずは下記の2つのポイントを意識してコードを書くことが重要です。
- POINT1:Response Bodyを必ずCloseする
- POINT2:Response Bodyを読み切る
POINT1:Response Bodyを必ずCloseする
具体的に必要なアクション
- Responseを受け取ったら、BodyをCloseすることが必要です。
- 一般的にdeferを使ってCloseしますが、forループの中では注意が必要です。
▼Response BodyをCloseする:サンプルコード
// Closeを必ず実施。
res, err := http.Get("https://test-example.com/api/v1/test")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
注意点
- Bodyの内容を使用しない場合であっても、一旦変数に格納してCloseするように心がけましょう。
- 理由:BodyをCloseできていないことになります。
▼NGなコード
// Responseを変数に格納しないと、Closeできなくなってしまう。
_, err := http.Get("https://test-example.com/api/v1/test")
if err != nil {
log.Fatal(err)
}
- BodyをCloseする前に、最初にエラーチェックをしましょう。
- 理由:nilのResponse BodyをCloseすると、panicが発生するためです。
▼NGなコード
res, err := http.Get("https://test-example.com/api/v1/test")
//bodyをcloseすることが先になると、nil pointer dereferenceでpanicになる。
defer res.Body.Close()
if err != nil {
log.Fatal(err)
}
なぜResponse BodyをCloseしなければいけないのか?
- ドキュメントにおいて、Response BodyをCloseするのは、呼び出し側の責任とされています。
The client must close the response body
引用元URL:https://pkg.go.dev/net/http#Client
- Response BodyをCloseしないと、ファイルディスクリプタが枯渇するリスクがあります。
tcpコネクションはpersistConnection構造体の中にnet.Connインターフェイスで保持しています。
BodyをCloseしないと同構造体のclosedフラグがセットされず、net.ConnがCloseされません。
すなわち、TCPコネクションがクローズされないために、そのまま続けてHttpリクエストを発行していくとファイルディスクリプタが枯渇することになります。
引用元URL:https://qiita.com/stk0724/items/dc400dccd29a4b3d6471
POINT2:Response Bodyを読み切る
Response Bodyを「読み切る」とは?
- Response Bodyを全てreadすること。(のようです。)
具体的に必要なアクション
- Response Bodyをio.ReadAllやio.Copyを使って最後まで読み取りましょう。
▼サンプルコード
res, err := http.Get("https://test-example.com/api/v1/test")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
//ReadAllでResponse Bodyを読み切る
b, err := io.ReadAll(res.Body)
fmt.Println(string(b))
注意点
- Response Bodyを使わない場合であったとしても、deferなどで最後まで読み取るアクションが必要です。(ioutil.Discardを使用する)
▼Response Bodyを使わないパターンのサンプルコード
res, err := http.Get("https://test-example.com/api/v1/test")
if err != nil {
log.Fatal(err)
}
defer func (){
// ioutil.Discardは書き込んだバイトを全て捨てる
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
}()
なぜResponse Bodyを読み切らなければいけないのか?
- Bodyは全て読み切る+Closeすることがドキュメントで記載されています。
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.
引用元URL:https://pkg.go.dev/net/http#Client
- keepAliveできずにコネクションが再利用されずに終了してしまう。その結果、接続のたびに新しい接続を作ってしまうため。(通信の効率化が実現できず、無駄が生じる。パフォーマンスが悪化する)
参考URL:https://milestone-of-se.nesuke.com/nw-basic/as-nw-engineer/keepalive-tcp-http
まとめ
これまでの内容をまとめます。net/httpパッケージを使い、リクエストを投げる際にResponseを安全に扱うためには、下記2点を意識しましょう。
- 1:Response Bodyを必ずCloseする
- 理由:ファイルディスクリプタの枯渇を未然に防ぐため
- 2:Response Bodyを読み切る
- 理由:keepAliveを維持し、パフォーマンスの悪化を未然に防ぐため
参考文献
https://pkg.go.dev/net/http#Client
http://golangbyexample.com/resposne-body-closed-golang/
https://qiita.com/stk0724/items/dc400dccd29a4b3d6471
https://christina04.hatenablog.com/entry/go-keep-alive
http://need4answer.blogspot.com/2015/06/golanghttpbody.html
https://milestone-of-se.nesuke.com/nw-basic/as-nw-engineer/keepalive-tcp-http