はじめに
最近 Go 言語を始めたのですが,「動くコードは書けるが本当にこれで良いのか?」と思うことが多かったです.
そこでGo 言語 100Tips の本を購入し,一通り読んでみました.
とても参考になることが多く,現在私が書いているコードで改善できそうな箇所の発見や,新たな知見(特に低レイヤーな知見)の獲得が多く有り,よかったです.
この本にもある通り,Go 言語自体は覚えることが少ないように感じるが,それゆえに気を遣うべきことは多い,ということがわかりました.
その中でも私的に意外だった HTTP Body の Close について紹介します.
本記事のまとめ
- Go 言語の Http Body は常に Close しないと,メモリリークを起こす.
- Body の読み込み処理を必要とせずともコネクションを維持したければ,読み込みすること
- 効率良く読み込みを行いたい場合は io.Copy(io.Discard,resp.Body)のようにして,データを読み込んで破棄するコードにすること.
Close とは?
Close とは File や通信のコネクションなどの資源に対する操作のことで,Close をしないとメモリリークにつながります.
そのため Go では io.Closer インターフェイスを実装した全ての構造体はある時点で Close すべきとのことです.
(os.File は GC にガベージコレクトされた際に自動 Close するようですが,GC の稼働がいつ行われるのかわからないため,明示的に Close することが良いとのことです.)
HTTP レスポンス Body 構造体の Close について
HTTP レスポンスの Body 構造体は io.Closer を実装しており,Close すべき対象となります.
Body はレスポンスがエラーでない場合は常にBody を Close する必要があるみたいです.
これは Body に興味がなく,Body を利用しなくても Close しなければいけないということです.
例えば以下はステータスコードを読んで返す関数ですが,Body が Close されていないためこのままだとリークします.
type handler struct {
client *http.Client
url string
}
func (h handler) getStatusCodeWithLeakBody() int {
req, _ := http.NewRequest("GET", h.url, nil)
resp, _ := h.client.Do(req)
// resp.BodyをCloseしていない
return resp.StatusCode
}
あるべき姿としては以下のコードのように常にクローズすることです
...
func (h handler) getStatusCode() int {
req, _ := http.NewRequest("GET", h.url, nil)
resp, _ := h.client.Do(req)
defer func() {
// Close すること
_ := resp.Body.Close()
}()
return resp.StatusCode
}
また,面白いと感じた点として,Body を Close するときの動作は Body を読み込んだかどうかによって内部の動作が異なるという点がありました.
- Body を読み込まずに Body を Close
コネクションも Close - Body を読み込んで Body を Close
コネクションは Close しないため再利用できる可能性あり
上記のため,keep-alive のようにコネクションを使い回したい場合は,Body の必要性に関わらず Body を読み込む必要があるとのことです.
本では以下のようなコードを推奨していました.ここで io.ReadAll よりも io.Copy と io.Discard の組み合わせの方が,コピーをせずに破棄するという動作のため効率的とのことです.
func (h handler) getStatusCode() int {
req, _ := http.NewRequest("GET", h.url, nil)
resp, _ := h.client.Do(req)
defer func() {
// ReadAllよりもCopyを利用して効率よくBody部を破棄
_, _ := io.Copy(io.Discard,resp.Body)
}()
return resp.StatusCode
}
実際に計測してみた
最後に Close しないとどのくらい違うのか,以下の簡単なプログラムで計測してみました.
func main() {
h := handler{
client: &http.Client{},
url: "http://localhost:8000",
}
for i := 0; i < 500; i++ {
if os.Args[1] == "leak" {
println(h.getStatusCodeWithLeakBody())
} else {
println(h.getStatusCode())
}
printStatistics()
}
}
以下は Close 無し,有りの GC が稼働する前のメモリ利用量と GC が稼働した後のメモリ利用量です.
# Close 無し
Alloc = 3451 KiB TotalAlloc = 3451 KiB Sys = 15999 KiB NumGC = 0
Alloc = 2449 KiB TotalAlloc = 3479 KiB Sys = 16831 KiB NumGC = 1
# Close 有り
Alloc = 3539 KiB TotalAlloc = 3539 KiB Sys = 16255 KiB NumGC = 0
Alloc = 229 KiB TotalAlloc = 3562 KiB Sys = 17087 KiB NumGC = 1
Close なしの場合はメモリ利用量が少ししか減っていませんが,Close 有りの場合はガクッと減っています.
これは Body を Close をしないことで Body と Response がメモリリークしているためと考えられます.
逆に Body を Close したケースでは,Body がメモリから解放されているため,GC 時に Response などに割り当てられているリソースもメモリから解放することができ,メモリ利用量がガクッと減っていると考えられます.(もし見当違い等があれば教えて欲しいです...)
最後に
今回 Go 100 の Tips 本を読み特に驚いた?箇所について記事を書きました.
Go ということを強調しましたが,他の言語の仕様についても気になりました.
(私の好きな Rust は所有権によってここら辺は解決している?)
また,今回初めての投稿記事でしたが,再度本を読み直したり,検証したりで,さらに知識が深まった気がしてよかったです.
これからも継続して投稿できればと思います.
ここまで読んでくださりありがとうございました.