もう忘れない!Go言語のdeferで関数の後処理を確実に実行する方法

2025年4月17日木曜日

Go言語

この記事の主役は、Go言語のdeferです!

Go言語を触り始めたばかりだと、「deferって何だ…? なんか後で実行されるらしいけど…」ってなりますよね。分かります、私も最初はそうでした!

プログラムを書いていると、ファイルを開いたり、ネットワークに接続したり、いろんな「準備」が必要になります。でも、準備したら必ず「後片付け」が必要なんです。この後片付け、ついうっかり忘れちゃうんですよね…。

そんな忘れん坊さん(失礼!)の強い味方が、今回紹介する`defer`なんです!

この記事を読めば、`defer`の基本的な使い方から、ちょっとした注意点までバッチリ分かります。

この記事で学べること

  • `defer`が何をするものなのか、どんな時に役立つのか
  • `defer`の基本的な書き方
  • ファイルクローズやロック解除での具体的な使い方
  • 複数の`defer`を書いたときの実行される順番
  • `defer`を使うときに気をつけるポイント

Go言語の「defer」とは?関数の後処理を確実に実行する仕組み

`defer`を一言でいうと、「関数が終わる時に、これを実行してね!」とGo言語にお願いしておく仕組みです。 

特に、プログラムの中で何かリソース(資源)を使ったら、使い終わった後にそれをきちんと「解放」する、つまり後片付けする必要があります。

例えば、以下のような場面です。

  • ファイルを開いたら、最後に閉じる
  • データベースに接続したら、最後に切断する
  • ロック(Mutex)を取得したら、最後に解除する

これらの後片付け処理は、処理が成功した時も、途中でエラーが発生した時も、どんな状況でも確実に実行したいですよね。

でも、関数の途中でエラー処理のために`return`したりすると、後片付けのコードまでたどり着かずに終わってしまう可能性があります。

`defer`は、そんな「後片付け忘れ」を防ぐために生まれました。

`defer`を使って後片付け処理を登録しておけば、関数がどんな形で終了しようとも、Go言語が責任を持ってその処理を実行してくれるんです。コードがシンプルになり、後片付け忘れの心配もなくなる、まさに一石二鳥の機能と言えるでしょう。

Go言語「defer」の基本的な書き方

`defer`の書き方はとってもシンプル。 `defer`キーワードの後に、関数が終わる時に実行したい関数呼び出しを書くだけです。

package main

import "fmt"

func main() {
    // "関数終了!"というメッセージを関数が終わる時に表示するよう登録
    defer fmt.Println("関数終了!")

    fmt.Println("こんにちは!")
    fmt.Println("deferのテスト中です。")
}

これを実行すると、以下のようになります。

こんにちは!
deferのテスト中です。
関数終了!

どうでしょう? `defer fmt.Println("関数終了!")` はコードの最初に書きましたが、実行されるのは`main`関数の一番最後になっていますね。

このように、`defer`で指定した処理は、それが書かれた場所に関わらず、関数が終了する直前に実行されるのです。

Go言語「defer」の実践的な使い方

基本的な書き方が分かったところで、次は実際にどんな場面で`defer`が活躍するのか、具体的なコード例を見ていきましょう。 `defer`の真価は、ここから発揮されますよ!

ファイルのクローズ処理とGo言語 defer

プログラムでファイルを扱う場合、`os.Open()`などでファイルを開いたら、処理が終わった後に必ず`file.Close()`でファイルを閉じる必要があります。

これを忘れると、プログラムが終了するまでファイルが開きっぱなしになり、他のプログラムがそのファイルを使えなくなったり、最悪の場合、OSのリソースを使い果たしてしまうことも…。考えただけでも恐ろしいですね。

`defer`を使わない場合、エラー処理なども考えると、こんな感じになるでしょうか。

package main

import (
	"fmt"
	"os"
)

func readFileWithoutDefer(filename string) error {
	file, err := os.Open(filename)
	if err != nil {
		return fmt.Errorf("ファイルを開けませんでした %w", err)
	}

	// ... ファイル読み込み処理 ...
	fmt.Println("ファイルの内容を読み込み中...")

	// 読み込みが終わったらファイルを閉じる
	err = file.Close()
	if err != nil {
		// クローズ時のエラーも考慮する必要がある
		return fmt.Errorf("ファイルを閉じられませんでした %w", err)
	}
	fmt.Println("ファイルは正常に閉じられました。")
	return nil
}

func main() {
	// 存在しないファイルを指定してエラーを起こしてみる
	err := readFileWithoutDefer("存在しないファイル.txt")
	if err != nil {
		fmt.Println("エラー発生:", err)
	}
	// 存在するファイル (例: main.go) で試す場合はファイルパスを修正してください
	// err = readFileWithoutDefer("main.go")
	// if err != nil {
	// 	fmt.Println("エラー発生:", err)
	// }
}

うーん、ファイルを閉じる処理が関数の最後に書かれていますね。

もし、`// ... ファイル読み込み処理 ...` の部分でエラーが発生して`return`したら、`file.Close()`は実行されません。 エラーが起きるたびに`file.Close()`を書くのは面倒ですし、忘れそうです。

そこで`defer`の出番です!

package main

import (
	"fmt"
	"os"
)

func readFileWithDefer(filename string) error {
	file, err := os.Open(filename)
	if err != nil {
		return fmt.Errorf("ファイルを開けませんでした %w", err)
	}
	// ★★★ ここがポイント! ★★★
	// ファイルを開いた直後に、閉じる処理をdeferで登録!
	defer file.Close()
	// defer文にエラーハンドリングを追加することも可能ですが、ここではシンプルにします。
	// defer func() {
	//     err := file.Close()
	//     if err != nil {
	//         fmt.Println("deferでのファイルクローズエラー:", err)
	//     }
	// }()


	// ... ファイル読み込み処理 ...
	fmt.Println("ファイルの内容を読み込み中...")
	// ここでエラーが発生してreturnしても、deferで登録したCloseは実行される!

	fmt.Println("ファイルの読み込み処理(仮)完了。")
	// 関数の最後に到達した場合も、もちろんdeferが実行される。
	return nil
}

func main() {
	// 存在しないファイルを指定
	err := readFileWithDefer("存在しないファイル.txt")
	if err != nil {
		fmt.Println("エラー発生:", err)
	}

	fmt.Println("---")

	// 存在するファイル (例: このファイル自身) を指定
	// 実行環境に合わせてファイル名は調整してください
	// 例: Windowsなら "go.mod" など存在するファイル名に
	err = readFileWithDefer("main.go") // このファイルが存在すると仮定
	if err != nil {
		fmt.Println("エラー発生:", err)
	} else {
		fmt.Println("ファイル処理は正常に完了しました。(deferによりCloseも実行されています)")
	}
}

実行結果の例(main.goが存在する場合):

エラー発生: ファイルを開けませんでした open 存在しないファイル.txt: no such file or directory
---
ファイルの内容を読み込み中...
ファイルの読み込み処理(仮)完了。
ファイル処理は正常に完了しました。(deferによりCloseも実行されています)

どうですか? `os.Open()`でファイルを開いたすぐ後に`defer file.Close()`と書いておくだけ。

これで、関数が正常に終わろうが、途中でエラーで`return`しようが、Go言語が責任を持って`file.Close()`を実行してくれるんです。コードがスッキリして、閉じ忘れの心配もなくなりました!これは使わない手はないですね。

Mutex(ロック)の解除とGo言語 defer

複数の処理(ゴルーチン)が同時に同じデータにアクセスしようとすると、データの整合性が取れなくなってしまうことがあります。

そういう時に使うのが`Mutex`(ミューテックス、相互排他ロック)です。 データを変更する前に`Lock()`で鍵をかけ、他の処理がアクセスできないようにし、処理が終わったら`Unlock()`で鍵を開ける、という使い方をします。

この`Unlock()`も、ファイルの`Close()`と同じで、絶対に実行し忘れてはいけない処理です。 もし`Unlock()`し忘れると、他の処理が永遠に待たされ続ける「デッドロック」という、とても厄介な状態になってしまいます。

ここでも`defer`が大活躍します!

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	count  int
	mutex sync.Mutex // Mutexを準備
)

// countを安全に増やす関数
func increment() {
	// 処理の開始時にロックを取得
	mutex.Lock()
	// ★★★ ここがポイント! ★★★
	// ロックを取得した直後に、Unlock処理をdeferで登録!
	defer mutex.Unlock()

	// --- ここからが保護したい処理 ---
	c := count
	c++
	// わざと少し待って、他のゴルーチンが割り込む隙を作る
	time.Sleep(10 * time.Millisecond)
	count = c
	// --- ここまでが保護したい処理 ---

	// Unlockはdeferに任せるので、ここに書く必要はない!
}

func main() {
	var wg sync.WaitGroup // ゴルーチンの完了を待つための仕組み

	// 10個のゴルーチンを起動して、それぞれincrementを実行
	for i := 0; i < 10; i++ {
		wg.Add(1) // WaitGroupのカウンタを増やす
		go func() {
			defer wg.Done() // ゴルーチン終了時にカウンタを減らすようdeferで登録
			increment()
		}()
	}

	// 全てのゴルーチンが完了するのを待つ
	wg.Wait()

	fmt.Println("最終的なカウント:", count) // 10になるはず
}

実行結果:

最終的なカウント: 10

`mutex.Lock()`を実行した直後に`defer mutex.Unlock()`と書くのが、Go言語の並行処理における定石パターンです。

これで、`increment`関数の中で何が起ころうとも(例えば、将来的にエラーで`return`するコードが追加されたとしても)、確実にロックが解除されることが保証されます。安全な並行処理のためには、`defer`は欠かせません。


複数のdeferがある場合の実行順序(LIFO)

一つの関数の中で、`defer`を複数回使うこともできます。その場合、どの順番で実行されるのでしょうか?

答えは、「最後に`defer`されたものから順に実行される」です。

プログラミングの世界では、こういう動きをLIFO(Last-In, First-Out)、日本語だと「後入れ先出し」と呼びます。 まるで、後から積み重ねたお皿を、一番上から順番に取っていくようなイメージですね。

実際にコードで確認してみましょう。

package main

import "fmt"

func main() {
	fmt.Println("関数の処理開始")

	defer fmt.Println("defer 1番目 (最初に書いた)")
	defer fmt.Println("defer 2番目")
	defer fmt.Println("defer 3番目 (最後に書いた)")

	fmt.Println("関数の処理終了直前")
}

実行結果:

関数の処理開始
関数の処理終了直前
defer 3番目 (最後に書いた)
defer 2番目
defer 1番目 (最初に書いた)

ほら、`defer`で登録した順番とは逆の順番、つまり最後に書いた「3番目」から順に実行されているのが分かりますね。

このLIFOの挙動は、例えばリソースの初期化と解放の順番が逆になるような場合に便利だったりします。(先に開いたものを後に閉じる、など)

LIFOのイメージ図(スタック)

関数実行中:

defer実行時:          関数終了時(defer実行順):
+---------------+        +---------------+
| defer 3番目   | ← PUSH | defer 1番目   | ← POP 3
+---------------+        +---------------+
| defer 2番目   | ← PUSH | defer 2番目   | ← POP 2
+---------------+        +---------------+
| defer 1番目   | ← PUSH | defer 3番目   | ← POP 1
+---------------+        +---------------+
      スタック              スタック(空になる)

Go言語「defer」の実行タイミングを正確に理解しよう

さて、`defer`が「関数が終わる時に実行される」ことは分かりました。 

もう少しだけ正確に言うと、`defer`で指定した処理(関数呼び出し)は、その`defer`文を含む関数がまさに終了しようとする直前に実行されます。

具体的には、

  • 関数内の処理が最後まで到達し、`return`文が実行された
  • 関数内で`panic`が発生した場合、`panic`の処理が始まる

のタイミングです。

関数の「出口」に門番がいて、関数から出ていこうとする時に「おっと、`defer`で頼まれてた仕事があったな」と思い出して実行してくれる、そんなイメージを持つと分かりやすいかもしれません。

`defer`文を書いた場所とは関係なく、必ず関数の出口で実行される、という点がポイントです。

Go言語「defer」を使う上での注意点

便利な`defer`ですが、いくつか知っておかないと思わぬ動きをしてしまうことがあります。

ここでは、特に初心者がつまずきやすい注意点を2つと、少し応用的なテクニックを1つ紹介しましょう。

【注意点1】deferへ渡す関数の引数はすぐに評価される

これは結構ハマりやすいポイントです!

`defer`キーワードを使って関数呼び出しを登録する際、その関数に渡す引数(値)は、`defer`文が実行されたその瞬間に評価され、値が確定(コピー)されます。 後で変数の値が変わっても、`defer`で実行される関数には影響しません。

言葉だけだと分かりにくいので、コードで見てみましょう。

package main

import "fmt"

func main() {
	i := 0
	// defer文が実行される時点での i の値 (0) が fmt.Println に渡される
	defer fmt.Println("defer実行時のiの値:", i)

	i = 10
	fmt.Println("関数終了直前のiの値:", i)
}

実行結果:

関数終了直前のiの値: 10
defer実行時のiの値: 0

あれ? `defer`は関数の最後に実行されるはずなのに、表示された`i`の値は`0`ですね。 これは、`defer fmt.Println(...)`と書いた時点で、引数`i`の値`0`が`fmt.Println`に渡されることが決まってしまったからです。

その後に`i = 10`と代入しても、`defer`で実行される`fmt.Println`が使う`i`の値は、最初に確定した`0`のまま、というわけです。

特にループの中で`defer`を使う場合は、この挙動に注意しないと、全部同じ値で`defer`が実行されてしまう…なんてことになりがちです。

もしループ変数などの「その時点での値」を`defer`で使いたい場合は、無名関数(関数リテラル)で囲むといったテクニックが必要になりますが、まずは「引数は`defer`を書いた瞬間に決まる!」と覚えておきましょう。

【注意点2】deferとpanic/recoverの関係

プログラム実行中に予期せぬエラーが発生し、プログラムの正常な続行が不可能になった状態を`panic`と呼びます。 Go言語では、`panic`が発生すると、プログラムは通常そこで停止してしまいます。

しかし、`defer`で登録された処理は、たとえ`panic`が発生した場合でも、その関数が終了する前に実行されるという、非常に頼もしい性質を持っています。

これにより、例えば以下のようなことが可能です。

  • `panic`が起きても、開いたファイルや取得したロックなどのリソースは確実に解放される。
  • `defer`内で`recover()`という組み込み関数を使うと、`panic`から復帰してプログラムの実行を継続させることも可能(高度なエラーハンドリング)。

`recover()`の詳細は少し複雑なのでここでは深入りしませんが、`panic`が起きても`defer`はちゃんと仕事してくれる、と覚えておくだけでも、`defer`への信頼感が増すのではないでしょうか。

【注意点3】defer内で関数の戻り値を変更できる(名前付き戻り値)

これは少し応用的な話になります。 Go言語の関数では、戻り値に名前を付けることができます(名前付き戻り値)。

func namedReturn() (result int) { // resultが名前付き戻り値
    result = 1
    return // resultが暗黙的に返される
}

そして、`defer`で実行される関数の中からは、この名前付き戻り値を参照したり、値を変更したりすることが可能なんです。

package main

import "fmt"

// 戻り値に result という名前を付ける
func modifyReturnValue() (result int) {
	defer func() {
		// deferの中で戻り値resultの値を変更する
		fmt.Println("defer実行前:", result)
		result = result * 2
		fmt.Println("defer実行後:", result)
	}()

	fmt.Println("関数本体: resultに1を代入")
	result = 1
	// return文(暗黙的に return result が実行される)
	// このreturnの後にdeferが実行される
	return
}

func main() {
	fmt.Println("最終的な戻り値:", modifyReturnValue())
}

実行結果:

関数本体: resultに1を代入
defer実行前: 1
defer実行後: 2
最終的な戻り値: 2

関数本体では`result`に`1`を代入して`return`していますが、その直後に実行された`defer`の中で`result`が`2`に変更されたため、最終的な関数の戻り値は`2`になっていますね。 

これは、関数の最後に必ず何かチェックを入れたり、戻り値に共通の加工を施したりしたい場合に使えるテクニックですが、コードが少し読みにくくなる可能性もあるので、使いどころは考えた方が良いかもしれません。「こんなこともできるんだな」程度に知っておくと良いでしょう

【まとめ】Go言語「defer」を使いこなして安全なコードを書こう

今回は、Go言語の便利な機能`defer`について、基本的な使い方から実行順序、注意点までを見てきました。 もう一度ポイントをおさらいしましょう。

  • `defer`は、関数の終了時に実行したい処理を登録する仕組み
  • ファイルのクローズやMutexのアンロックなど、後片付け処理に最適
  • 複数の`defer`は、最後に登録されたものから順(LIFO)に実行される
  • `defer`に渡す関数の引数は、`defer`文実行時に評価される点に注意
  • `panic`が発生しても`defer`は実行される

`defer`を使いこなせば、リソースの解放漏れなどを防ぎ、コードの可読性も安全性もグッと高まります。 最初は少し戸惑うかもしれませんが、積極的に使ってみて、その便利さを体感してください!

`defer`をマスターすれば、Go言語でのプログラミングがもっと楽しく、そして効率的になるはずです。自信を持って、どんどん活用していきましょう!

【関連記事】 Go言語とは?特徴・できること・将来性

このブログを検索

  • ()

自己紹介

自分の写真
リモートワークでエンジニア兼Webディレクターとして活動しています。プログラミングやAIなど、日々の業務や学びの中で得た知識や気づきをわかりやすく発信し、これからITスキルを身につけたい人にも役立つ情報をお届けします。 note → https://note.com/yurufuri X → https://x.com/mnao111

QooQ