deferは、周囲の関数からの退出まで関数の実行を遅らせます — パニックやreturnによって退出された場合でもです。ループ内でdeferを使用すると、すべての遅延呼び出しがdefer呼び出しのスタックに蓄積され、周囲の関数が終了する際に逆順で実行されます。
これにより予期しないリソースの消費や遅延が発生する可能性があります。なぜなら、すべての遅延関数は関数の本体を終了した後だけに一度に呼び出されるからです。ループの各反復の後に呼び出されるわけではありません。
コードの例:
func readFiles(files []string) { for _, name := range files { f, _ := os.Open(name) defer f.Close() // リソースは関数全体が終了するまで解放されません // ファイルの処理 ... } }
この例では、ファイルは関数の実行が完了するまでオープンのままであり、大量のファイルがあるときにディスクリプタのリークを引き起こす可能性があります。
ループ内でリソースを閉じるためにdeferを使用した場合、何が起こりますか?なぜこれは常に最適ではありませんか?
回答: すべてのdefer呼び出しが蓄積され、関数が終了するまで実行されません。これは、リソース(例えばオープンファイル)が遅すぎるタイミングで解放されることにつながります。
正しい:
for _, name := range files { f, _ := os.Open(name) // defer f.Close() // ダメ! // 正しい: process(f) f.Close() }
物語
ログのアップロードプロジェクトで問題が発生しました: サービスが突然新しいファイルを開かなくなりましたが、ファイルは少なかったです。原因はループ内のdeferでした。すべてのファイルが開かれ、クローズが関数の最後まで遅れました。処理後に明示的にClose()を書くようにしたら問題が消えました。
物語
大きなリストに対してメトリクスを収集するサービスでは、データを巡回するループ内でデータベースへの接続をリセットするためにdeferが使用されていました。反復が増えるにつれて遅延が発生し、オープン接続のしきい値を超えてサービスが「too many open connections」のエラーでクラッシュしました。
物語
エンジニアは「エレガント」なクリーンアップを期待して多くのファイルを読み取るループ内でdeferを使用し、結果として本番環境のサーバー上でオープンディスクリプタの制限が超過し、サービスが停止しました。