defer

【Go言語】deferとpanic:リカバリ処理でプログラムを安定化する手法

この記事では、Go言語におけるdeferpanicの使い方について、シンプルなエラーハンドリングの方法を解説します。

基本的な開発環境が整っている方を対象に、実行時の流れや注意点を分かりやすく説明していきます。

deferの基本

deferの動作メカニズム

deferは、関数の終了直前に指定した処理を実行する仕組みです。

関数内でdeferを使うと、定義した順番とは逆の順序(LIFO)で実行されます。

評価はdefer宣言時に行われ、引数の評価や関数の参照はその時点で確定されます。

以下のサンプルコードは、deferを使って後処理を登録する方法を示しています。

package main
import "fmt"
func main() {
	// 複数のdeferを定義(後に定義したものが先に実行される)
	defer fmt.Println("後処理1") // 先に登録したdefer
	defer fmt.Println("後処理2") // 後に登録したdefer
	fmt.Println("処理開始")
}
処理開始
後処理2
後処理1

deferの実行タイミング

deferで登録した処理は、関数の終了時に実行されます。

関数が正常に終了した場合も、panicによって異常終了した場合も、必ず実行されるのが特徴です。

これにより、ファイルのクローズ処理やリソースの解放など、後処理を確実に実行することが可能です。

例えば、deferによる後処理は関数内のreturn直前や、panic発生時でも実施されます。

以下の例は、通常終了の場合とpanic発生時の動作を確認できます。

package main
import "fmt"
func demoDefer() {
	defer fmt.Println("deferは必ず実行される")
	// 正常な場合
	if true {
		fmt.Println("正常処理")
		return
	}
	// ここに到達することはない
	panic("panic発生")
}
func main() {
	demoDefer()
}
正常処理
deferは必ず実行される

deferのネスト処理の挙動

複数のdeferがネストしている場合、各関数の終了時にその関数に登録されたdeferが実行されます。

ネストされた関数が終了すると、その関数内のdeferが実行され、呼び出し元へ処理が戻ります。

最終的に、呼び出し元側で登録されたdeferが実行されます。

以下のサンプルコードは、ネストした関数内でのdeferの動作を示しています。

package main
import "fmt"
func nestedDefer() {
	// 外側のdeferは関数終了時に実行される
	defer fmt.Println("外側のdefer")
	func() {
		// 内側のdeferはその無名関数の終了時に実行される
		defer fmt.Println("内側のdefer")
		fmt.Println("内側処理")
	}()
	fmt.Println("外側処理")
}
func main() {
	nestedDefer()
}
内側処理
内側のdefer
外側処理
外側のdefer

panicの基本

panic発生の条件

panicは、回復が困難な状況や致命的なエラーが発生した場合に、プログラムの実行を中断するために使われます。

明示的にpanic("エラーメッセージ")と記述するか、標準ライブラリ内のエラー処理で暗黙的にpanicが呼ばれる場合があります。

通常のエラーハンドリングとは異なり、緊急時にのみ使用することが望ましいです。

以下は、panicを発生させるシンプルな例です。

package main
import "fmt"
func main() {
	// エラー発生時にpanicを呼び出す
	panic("致命的エラー発生")
}
panic: 致命的エラー発生
goroutine 1 [running]:
main.main()
	/path/to/file.go:7 +0x39

panicの伝播と挙動

panicが発生すると、その関数の残りの処理は中断され、呼び出し元へ伝播します。

伝播する過程で、その関数内に登録されたdeferがすべて実行されます。

呼び出し元でも同様に、panicに伴ってdeferが実行され、最終的にpanicがmain関数まで到達するとプログラムは終了します。

次のサンプルコードは、ネストした関数でpanicが発生した場合の伝播とdeferの実行順序を示しています。

package main
import "fmt"
func subFunction() {
	defer fmt.Println("subFunctionのdefer処理")
	// panicを発生させる
	panic("subFunctionでパニック発生")
}
func main() {
	defer fmt.Println("mainのdefer処理")
	subFunction()
}
subFunctionのdefer処理
mainのdefer処理
panic: subFunctionでパニック発生
goroutine 1 [running]:
main.subFunction()
	/path/to/file.go:6 +0x39
main.main()
	/path/to/file.go:11 +0x39

プログラム終了に至る流れ

panicが回復されずに伝播した場合、プログラムは最終的に異常終了します。

この際、各関数で登録されたdeferは全て実行されますが、その後、エラーメッセージとスタックトレースが表示され、プログラムが終了します。

以下の例は、panicが発生した場合の挙動を示しています。

package main
import "fmt"
func main() {
	defer fmt.Println("deferは必ず実行される")
	// パニック発生によりプログラム終了へ
	panic("終わりの無いパニック")
}
deferは必ず実行される
panic: 終わりの無いパニック
goroutine 1 [running]:
main.main()
	/path/to/file.go:7 +0x39

deferとpanicの連携

defer内でのpanic対処

recover関数の役割と使い方

recoverは、panicで発生したエラーを捕捉し、プログラムの異常終了を防ぐために活用されます。

recoverは必ずdefer内で呼び出す必要があります。

捕捉に成功すると、panicの値が返され、以降の処理を継続することが可能になります。

以下のサンプルコードは、recoverを使ってpanicを回復する方法を示しています。

package main
import "fmt"
func safeFunction() {
	defer func() {
		// recoverでpanicを捕捉し、エラーを処理する
		r := recover()
		if r != nil {
			fmt.Println("panicをrecoverしました:", r)
		}
	}()
	fmt.Println("処理開始")
	// パニックを発生させる
	panic("エラー発生!")
	// この処理は実行されない
	fmt.Println("処理終了")
}
func main() {
	safeFunction()
	fmt.Println("プログラム継続")
}
処理開始
panicをrecoverしました: エラー発生!
プログラム継続

deferとpanicの連結動作

deferで登録した複数の関数がある場合、定義した逆順に実行されます。

panic発生時も同様に、後から登録したdeferが先に実行されます。

中には、recoverを呼び出してエラーを捕捉するdefer関数も存在します。

その際、他のdefer関数の実行順序に注意する必要があります。

以下のコードは、複数のdeferが連携して動作する例です。

package main
import "fmt"
func multipleDefer() {
	defer fmt.Println("後処理: 1")
	defer func() {
		// recoverでpanicを捕捉
		r := recover()
		if r != nil {
			fmt.Println("recoverで捕捉:", r)
		}
	}()
	defer fmt.Println("後処理: 2")
	fmt.Println("処理開始")
	// panic発生
	panic("エラー発生")
	// この行は実行されない
	fmt.Println("処理終了")
}
func main() {
	multipleDefer()
	fmt.Println("プログラム継続")
}
処理開始
後処理: 2
recoverで捕捉: エラー発生
後処理: 1
プログラム継続

エラーハンドリングへの応用例

実際のコード例による検証

panicrecoverを組み合わせることで、エラーハンドリングの一環として予期しない状況に対処することができます。

例えば、入力の数値変換時に非数値が渡された場合、panicを発生させ、recoverでエラーを捕捉する方法があります。

package main
import (
	"fmt"
	"strconv"
)
func convertStringToInt(s string) int {
	defer func() {
		if r := recover(); r != nil {
			// panicをrecoverしてエラーをログ出力する
			fmt.Println("recoverでエラーを処理:", r)
		}
	}()
	// 文字列が数字でない場合、panicを発生させる
	if _, err := strconv.Atoi(s); err != nil {
		panic("変換エラー")
	}
	number, _ := strconv.Atoi(s)
	return number
}
func main() {
	fmt.Println("正常な変換:")
	num := convertStringToInt("123")
	fmt.Println("変換結果:", num)
	fmt.Println("エラーのある変換:")
	convertStringToInt("abc")
	fmt.Println("プログラム継続")
}
正常な変換:
変換結果: 123
エラーのある変換:
recoverでエラーを処理: 変換エラー
プログラム継続

注意事項とポイント

  • recoverは必ずdefer内で呼び出す必要があり、panicの入力値を取得する手段である。
  • 多段のdeferがある場合、登録順の逆順で実行されるため、意図したタイミングでrecoverが呼ばれるよう注意すること。
  • panicrecoverは、本来回復不可能な状態に対する対策として設計されているため、通常のエラーハンドリングにはエラー返却の仕組みを使用することが望ましい。

応用例と留意点

開発現場での利用例

実際の開発現場では、ネットワーク通信やデータベース接続といった外部リソースを扱う際に、確実な後処理が求められる場面や、予期せぬエラーに対する安全措置としてdeferpanicが活用されることがあります。

例えば、リソースを確実に解放したり、エラーログを記録してサービスの継続を図るといった用途に利用されます。

以下のサンプルコードは、外部リソースの処理中にpanicが発生した場合、deferrecoverでエラーを捕捉し、ログ出力を行いながらプログラムの継続を図る例です。

package main
import "fmt"
func processData() {
	defer func() {
		// recoverでpanicを捕捉し、エラーログとして出力
		if err := recover(); err != nil {
			fmt.Println("エラーログ:", err)
		}
	}()
	fmt.Println("データ処理開始")
	// ここでエラーを模擬してpanicを発生させる
	panic("データ破損エラー")
	// この行は実行されない
	fmt.Println("データ処理終了")
}
func main() {
	processData()
	fmt.Println("サービス継続")
}
データ処理開始
エラーログ: データ破損エラー
サービス継続

よくある落とし穴と対策

  • defer内でrecoverを確実に呼び出さない場合、panicが回復せずプログラムが想定外に終了してしまう可能性があるため、必ずdefer内で呼び出すよう設計する。
  • 複数のdeferがある際、呼び出し順序により思わぬ動作になることがあるため、登録順序と実行順序を意識してコードを書くことが重要です。
  • panicrecoverを過度に使用すると、コードの可読性や保守性が低下する場合があるため、通常のエラーハンドリングと効果的に使い分けることが望ましいです。

まとめ

この記事では、deferとpanicの基本動作、実行タイミング、ネスト時の挙動、連携方法やエラーハンドリングへの応用例について詳しく解説しました。

全体を通して、Go言語における後処理と異常時のエラー対策の仕組みが体系的に把握できる内容でした。

ぜひ、実際のコードを試しながら自身のプロジェクトに応用してみてください。

Back to top button
目次へ