deferはリソース(たとえば、ファイル、mutex、接続)を管理するのを簡素化するためにGoで導入されました。これは、関数の実行が終了する直前に操作を実行することを保証する必要があるすべてのケースに対してです。歴史的に、他の言語にも類似の構文が存在しました(Javaのfinally、try-with-resources)が、Goはより明確で理解しやすいパターンを実装しています。
問題:リソースが解放されることを常に確認する必要があります。エラーが発生する場合やpanicが発生する場合でも。また、リソースの二重解放やメモリリークは、従来型プログラミングの一般的な問題です。
解決策:関数またはメソッド内でdeferを介して宣言されたすべてのものは、呼び出しスタックに配置され、関数から出る前に逆順で実行されます。これにより、例外(panic)や予期せぬreturnがあってもリソースが解放されることが保証されます。
コードの例:
func processFile() error { f, err := os.Open("filename.txt") if err != nil { return err } defer f.Close() // ファイルは最後に閉じられます // ファイルでの作業 return nil }
主な特徴:
関数内でpanicが発生した場合、deferは実行されるのか?
はい!すべてのdefer関数はpanicが発生した場合でも呼び出されます。これは「ファイナリゼーション」の基本メカニズムです。
deferに渡した関数の引数はいつ計算されるのか?
deferの宣言時に計算され、実際に実行される時ではありません。したがって、以降に変更される変数を使用する場合は注意が必要です:
a := 1 defer fmt.Println(a) a = 2 // 1が表示され、2ではない
ループ内でdeferはどのように機能するのか?メモリリークを引き起こさないか?
ループの各イテレーションでdeferを使用すると、すべてのdeferは関数全体の終了後にのみ実行され、各イテレーションの後ではありません。すべてのdefer関数がスタックに蓄積され、過剰なメモリ消費につながる可能性があります。
for i := 0; i < 3; i++ { defer fmt.Println(i) }
ループで千のファイルを開き、各々にdeferを使用すると、すべてのファイルは関数の最後にのみ閉じられ、リソースが保持され、ファイルオープンの制限を超える「リーク」が発生します。
利点:
欠点:
ループではローカル関数を使用し、deferはこのファイルのスコープのためだけに適用され、全体のハンドラーではありません。
for _, name := range fileNames { func() { f, _ := os.Open(name) defer f.Close() // fでの作業 }() }
利点:
欠点: