
この記事では筆者のようなJavaやC#をメインで利用している方向けにGoの書き方を説明しています。 書きっぷりがGoはjava/C#よりすごいぜって感じになってますがご容赦ください。
【Go言語】Java/C#エンジニアが「finally」から解放される魔法の言葉 defer
JavaやC#でリソース管理(ファイルの読み書き、DB接続、ロックの取得など)を行う際、コードがネスト地獄になったり、try-catch-finally が長くなりすぎて「本来の処理」がどこにあるか見失ったりした経験はありませんか?
Go言語の defer は、そんな悩みを解決する強力なキーワードです。今回はこの defer を、Javaの try-finally や C#の using ステートメントと比較しながら解説します。
1. 一言で言うと:「予約された後始末」
defer は、「この関数(メソッド)が終了する直前に、必ずこの処理を実行してね」 と予約する命令です。
JavaやC#の感覚で言うと、「関数全体を包む巨大な try-finally ブロックを、たった1行で宣言する」 ようなものです。
2. Java/C# との比較:ファイルの読み込み
「ファイルを開いて、何か書き込んで、最後に必ず閉じる」という処理を比べてみましょう。
Java (Classic Style) / C# (Old Style)
リソース解放のために finally ブロックが必須です。変数のスコープを気にする必要もあり、記述が冗長になりがちです。
// Java FileInputStream fis = null; try { fis = new FileInputStream("test.txt"); // ... 処理 ... } catch (IOException e) { // ... エラー処理 ... } finally { if (fis != null) { try { fis.close(); } catch (IOException e) {} } }
C# (using statement) / Java (try-with-resources)
これらは非常に優秀な構文ですが、「ネスト(インデント)」が深くなるという欠点があります。
// C#
using (var reader = new StreamReader("test.txt"))
{
// ... ここで処理 ...
// インデントが1段深くなる
} // ここで自動的に Dispose() される
Go (defer)
Goには try-finally も using もありません。代わりにこう書きます。
func processFile() error { f, err := os.Open("test.txt") if err != nil { return err } // 【ここがポイント!】 // 開いた直後に「関数終了時に閉じること」を予約 defer f.Close() // ... 処理 ... // ... 処理 ... return nil // ここで自動的に f.Close() が実行される }
- ネストが深くならない:
usingの波括弧{}が不要です。 - 宣言と解放がセット:
Openした直後にCloseを書けるため、「閉じ忘れ」を目視で確認しやすくなります。処理が何百行続いても、リソース管理のコードが離れ離れになりません。
3. defer の重要な挙動:LIFO(後入れ先出し)
defer を複数書いた場合、どのような順序で実行されるのでしょうか?
ここがスタック(Stack)構造になっています。
func main() { defer fmt.Println("1番目: データベース切断") defer fmt.Println("2番目: ファイルクローズ") defer fmt.Println("3番目: 計算ログ出力") fmt.Println("メインの処理実行中...") }
出力結果:
メインの処理実行中... 3番目: 計算ログ出力 2番目: ファイルクローズ 1番目: データベース切断
Java/C#で、ネストした using ブロックや try-finally が内側から順に解放されていくのと同じ順序(逆順)です。
「最後に開いたものを最初に閉じる」というリソース管理の鉄則が、構文レベルで保証されています。
4. よくある落とし穴:引数の評価タイミング
Java/C#エンジニアが最も引っかかりやすいのがここです。
「defer に渡した関数の引数は、defer を宣言した瞬間に評価(固定)される」 というルールがあります。
クイズ
以下のコードは最後に何を出力するでしょうか?
func demo() { i := 10 defer fmt.Println("deferの値:", i) // ここで宣言 i = 20 // 後で値を変更 fmt.Println("現在の値:", i) }
答え
クリックで表示
現在の値: 20 deferの値: 10
「あれ、最後は20じゃないの?」と思うかもしれません。
しかしGoは、defer fmt.Println(..., i) を通過した瞬間の i の値(つまり10)をコピーして保存します。そのため、後で i がどう変わろうと、終了時には 10 が出力されます。
回避策(ポインタやクロージャを使う): もし「終了時の最新の値」を使いたい場合は、無名関数(クロージャ)を使います。
defer func() { // 関数実行時に i を参照しにいく fmt.Println("deferの値:", i) }()
これなら 20 が出力されます。C#のラムダ式における変数のキャプチャに近い挙動ですね。