【ITニュース解説】Understanding defer in Go: Best Practices, Common Pitfalls, and Why It Matters
2025年09月11日に「Dev.to」が公開したITニュース「Understanding defer in Go: Best Practices, Common Pitfalls, and Why It Matters」について初心者にもわかりやすく解説しています。
ITニュース概要
Go言語の`defer`は、関数が終了する直前に指定した処理を実行する機能だ。複数ある場合、後から登録されたものが先に実行される。ファイルクローズなど、確実なリソース解放やエラー時のクリーンアップに役立ち、コードの信頼性を高める。
ITニュース解説
Go言語には、特定の処理を関数の実行が終了する直前まで遅らせるための特別な仕組みであるdeferキーワードがある。これは、コードの可読性を高め、リソース管理を簡素化するために非常に役立つ機能である。システムエンジニアを目指す上で、このような言語の特性を深く理解することは、堅牢で保守しやすいプログラムを書くために不可欠である。
deferの公式な定義は「deferは、囲んでいる関数が戻った直後に実行されるように関数呼び出しをスケジュールする」というものである。これは、deferキーワードが前置された関数呼び出しは、すぐに実行されるのではなく、その呼び出しが行われた親関数が終了する直前まで待機し、そのタイミングで実行されるという意味である。deferされた関数呼び出しは、特別な「遅延呼び出しスタック」に積まれ、親関数が終了する際には、このスタックから呼び出しが取り出されて実行される。このとき、遅延呼び出しは「Last-In, First-Out (LIFO)」の順序で実行される。つまり、最後にdeferされた関数が最初に実行され、最初にdeferされた関数が最後に実行される。
具体的なコード例を通して、deferの動作を詳しく見てみよう。例えば、以下のようなGoコードがあるとする。
1package main 2import "fmt" 3func main() { 4 a := 54 5 b := 34 6 c := a + b 7 fmt.Println(c) 8 defer fmt.Printf("from defer: %v\n", a + b) 9 defer fmt.Println("hello world from defer") 10 fmt.Printf("Hello, World!\n") 11}
このコードが実行されると、出力は次のようになる。
88
Hello, World!
hello world from defer
from defer: 88
この結果は、deferのユニークな振る舞いを明確に示している。main関数の実行フローを段階的に分析すると、その理由が理解できる。
ステップ1:即時実行
まず、変数aに54、bに34が代入され、cにはa + bの結果である88が計算される。そして、fmt.Println(c)が実行され、即座に「88」が出力される。
ステップ2:関数の遅延
次に、二つのdefer文に遭遇する。一つ目はdefer fmt.Printf("from defer: %v\n", a + b)、二つ目はdefer fmt.Println("hello world from defer")である。ここで重要なのは、deferされた関数の「引数」は、defer文に遭遇したその瞬間に評価されるという点である。つまり、最初のdefer文のa + bはここで88として評価され、二つ目のdefer文の"hello world from defer"という文字列もこの時点で確定する。しかし、これらの関数自体はまだ実行されず、遅延呼び出しスタックに積まれる。この時、fmt.Printf("from defer: %v\n", 88)がスタックに最初に入り、次にfmt.Println("hello world from defer")がスタックに積まれることになる。
ステップ3:通常の実行の継続
deferされた処理がスタックに積まれた後も、main関数内の通常の処理は続行される。したがって、fmt.Printf("Hello, World!\n")が実行され、「Hello, World!」が出力される。
ステップ4:遅延された関数の実行(LIFO順)
main関数内のすべての通常の処理が完了し、main関数が戻ろうとする直前になって、遅延呼び出しスタックに積まれていた関数が実行される。LIFOの原則に従い、最後にスタックに積まれたものが最初に実行される。この例では、fmt.Println("hello world from defer")が最初に実行され、「hello world from defer」が出力される。次に、最初にスタックに積まれたfmt.Printf("from defer: %v\n", 88)が実行され、「from defer: 88」が出力される。
この一連の流れにより、前述の出力結果が得られる。この例から、deferの重要な三つの特性を理解できる。第一に、deferされた関数に渡される引数は、defer文が実行された時点で評価される。第二に、deferされた関数は、Last-In, First-Out (LIFO) の順序で実行される。第三に、deferされた関数は、それを囲む関数が戻る直前に実行される。
deferは、リソースのクリーンアップ、特にファイルやネットワーク接続などの外部リソースを扱う際に非常に有用である。例えば、Go言語でファイルを操作する場合、ファイルを開いた後に確実に閉じる必要がある。
1package main 2import ( 3 "fmt" 4 "os" 5) 6func main() { 7 file, err := os.Open("example.txt") 8 if err != nil { 9 fmt.Println("Error opening file:", err) 10 return 11 } 12 defer file.Close() 13 fmt.Println("File opened successfully.") 14}
このコードでは、os.Openでファイルを開き、エラーがあればメッセージを表示して関数から早期にreturnする。しかし、ファイルが正常に開かれた場合、defer file.Close()が実行される。このdefer文があることで、main()関数のどの場所でreturnが行われたとしても、file.Close()が確実に実行されることが保証される。これにより、開発者はファイルを閉じる処理を忘れずに書くことができ、リソースリーク(使ったリソースが解放されない状態)を防ぐことができる。これは、プログラムの堅牢性を高める上で非常に重要な機能である。
deferのLIFO実行順序という特性は、単なるリソース管理だけでなく、工夫次第でさまざまな応用にも利用できる。例えば、ループや追加の配列を使わずに文字列を反転させることも可能である。
1package main 2import "fmt" 3func main() { 4 word := "golang" 5 fmt.Print("Reversed word: ") 6 for i := 0; i < len(word); i++ { 7 ch := word[i] 8 defer fmt.Printf("%c", ch) 9 } 10}
このコードでは、"golang"という文字列の各文字をループで取り出し、defer fmt.Printf("%c", ch)を使って文字を一つずつ遅延実行スタックに積んでいる。chはループの各イテレーションで異なる文字をキャプチャする。ループが終了し、main()関数が戻ろうとする直前になると、スタックに積まれたdefer関数がLIFO順で実行される。つまり、最後に積まれた文字「g」が最初にプリントされ、次に「n」、そして「a」と続き、最終的に「gnalog」という逆順の文字列が出力される。
deferキーワードは、Go言語においてシンプルでありながら非常に強力な機能である。その基本的な動作原理である「引数の即時評価」と「LIFO順での遅延実行」、そして「囲む関数が戻る直前の実行」を理解することは、ファイルやネットワーク接続といったリソースの確実なクリーンアップを行う上で不可欠である。さらに、その特性を応用することで、文字列反転のような処理も簡潔に記述できる。deferを効果的に活用することで、より安全で、読みやすく、保守しやすいGoコードを書くことができるだろう。システムエンジニアとして、Go言語で開発を進める際には、このdeferの力を最大限に引き出すことを意識すると良い。