【Go言語】Java/C#エンジニアが「finally」から解放される魔法の言葉 `defer`とは

この記事では筆者のようなJavaC#をメインで利用している方向けにGoの書き方を説明しています。 書きっぷりがGoはjava/C#よりすごいぜって感じになってますがご容赦ください。

【Go言語】Java/C#エンジニアが「finally」から解放される魔法の言葉 defer

JavaC#でリソース管理(ファイルの読み書き、DB接続、ロックの取得など)を行う際、コードがネスト地獄になったり、try-catch-finally が長くなりすぎて「本来の処理」がどこにあるか見失ったりした経験はありませんか?

Go言語の defer は、そんな悩みを解決する強力なキーワードです。今回はこの defer を、Javatry-finallyC#using ステートメントと比較しながら解説します。


1. 一言で言うと:「予約された後始末」

defer は、「この関数(メソッド)が終了する直前に、必ずこの処理を実行してね」 と予約する命令です。

JavaC#の感覚で言うと、「関数全体を包む巨大な 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-finallyusing もありません。代わりにこう書きます。

func processFile() error {
    f, err := os.Open("test.txt")
    if err != nil {
        return err
    }
    // 【ここがポイント!】
    // 開いた直後に「関数終了時に閉じること」を予約
    defer f.Close() 

    // ... 処理 ...
    // ... 処理 ...

    return nil 
    // ここで自動的に f.Close() が実行される
}

Java/C#エンジニアが感じるメリット:

  1. ネストが深くならない: using の波括弧 {} が不要です。
  2. 宣言と解放がセット: 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#ラムダ式における変数のキャプチャに近い挙動ですね。


まとめ

  • Goの defer は、Javatry-finallyC#using の代わりになる、よりフラットで可読性の高いリソース管理機構です。
  • 「開いたらすぐ defer Close() という手癖をつけるだけで、リソースリークを劇的に減らせます。
  • 実行順序はLIFO(逆順) なので、依存関係のあるリソース解放も安全です。