社の勉強会で指摘されたことを自戒の意を込めてポストする。
Defer おさらい
defer ステートメントは、 defer へ渡した関数の実行を、呼び出し元の関数の終わり(returnする)まで遅延させるものです。
上記は Tour of go より。HTTPクライアントの実装やファイルの読み書きをするときにリソースのクローズする際などによく使われる。
例えば以下のようなコード。
func ReachableDefer() (err error) {
resp, err := http.Get("http://sample.com/")
if err != nil {
return err // 遅延実行されない
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Fatal("close resources", err)
}
}()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return err
}
defer
の宣言の手前に return
の宣言がある場合、その return
実行時には defer
の処理は実行されない。
故に、上記コードにおいては http.Get("http://sample.com/")
の読み込みが失敗した場合、defer
の遅延実行は走らない。
この時、resp
は nil
となるのでリソースをクローズしなくても問題にはならない。
Defer を宣言する位置に注意
さきほどのコードにおいて defer
の宣言をうっかり下記のようにすると、読み込んだリソースが閉じられることなく処理が終了してしまい、読み込んだリソースがリークしてしまう。
func UnreachableDefer() (err error) {
resp, err := http.Get("http://sample.com/")
if err != nil {
return err // 遅延実行されない, resp は空なのでOK.
}
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
return err // 遅延実行されない, resp はクローズされずにリークするのでNG.
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Fatal("close resources", err)
}
}()
return err
}
ioutil.ReadAll(resp.Body)
の処理が失敗した場合は遅延実行は走らない。
最後に
いつ如何なるときも関数の終了時に遅延実行してくれるものと勘違いしてました。
学び。