はじめに
「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」7日目の記事は、Goのエラー処理と、それに関連するプログラムの制御方法についてです。
Goでコードを書き始めると、if err != nilというお決まりのフレーズの他に、panic、fatal、deferという挙動が一気に出てきます。
私自身、これらの挙動の違いが曖昧なので明確にするために、この記事では、Goの制御フローを理解する上で非常に重要な**log.Fatal、 panic、deferに焦点を当て、それぞれの役割と挙動の違い、そして適切な使い分けを、具体的なコード例と共に解説します。
- Goの基本的なエラーハンドリング (
if err != nil) は知っているが、その先の処理に迷う方 -
panicとlog.Fatalの違いがよくわからない方 -
deferの便利な使い方や注意点を知りたい方
この記事で、panic, log.Fatal, defer の役割を明確に理解して、状況に応じて適切なエラーハンドリングができるように、頑張りましょう!
それぞれの役割の違い
まず、それぞれの役割と挙動の違いを表で見てみましょう。
| プログラム | 主な役割 | 実行後の挙動 |
deferの実行 |
|---|---|---|---|
log.Fatal |
ログ出力 + プログラム終了 | 即座にプログラムが終了 (exit status 1) | 実行されない |
panic |
異常事態の通知 | 現在の関数の実行を停止し、コールスタックを遡り、リカバーされなければプログラムはクラッシュ | 実行される |
defer |
関数の終了処理の予約 | 関数がreturnするか、panicで終了する直前に実行される |
- |
それでは、一つずつ詳しく見ていきましょう。
log.Fatal とは?
log.Fatal(やlog.Fatalf)は、メッセージをログに出力した後、プログラムを即座に強制終了させるための関数です。
内部的にはos.Exit(1)を呼び出しており、これはOSに対して「プログラムが異常終了した」ことを伝えます。
log.Fatalの最も重要な特徴は、deferで予約した処理が実行されないことです。
とにかくプログラムを問答無用で終了させます。
main.goで以下の処理を実行してみましょう。
package main
import (
"fmt"
"log"
"os"
)
func main() {
defer fmt.Println("このdeferは実行されません!")
_, err := os.Open("non-existent-file.txt")
if err != nil {
log.Fatalf("ファイルを開けませんでした。アプリケーションを終了します: %v", err)
}
fmt.Println("この行は実行されません。")
}
実行結果は以下のようになります。
2025/12/24 10:00:00 ファイルを開けませんでした。アプリケーションを終了します: open non-existent-file.txt: no such file or directory
exit status 1
deferで指定したメッセージが表示されることなく、プログラムが終了しているのがわかります。
この挙動から、log.Fatalの使いどころはプログラムの起動時に限定するのが一般的です。
- 設定ファイルの読み込み失敗
- データベースへの接続失敗
など、「この処理が失敗したら、アプリケーションを起動しても意味がない」という致命的な状況で使います。
大抵はエラーメッセージとかを返さなければいけないので、あまり利用する場面はないかもしれません。
panic とは?
panicは、プログラムが予期せぬ異常状態に陥ったことを示すための仕組みです。log.Fatalのように即座に終了するのではなく、少し特殊な制御フローを辿ります。
挙動とdeferとの関係
panicが発生すると、以下の順で処理が進みます。
- 現在の関数の実行が停止します。
- その関数内で
deferされていた処理が実行されます。 - 呼び出し元の関数に処理が戻り、そこでも
panicが発生したかのように振る舞います(パニックの連鎖)。 - この連鎖がgoroutineの最上層まで達すると、コールスタックを出力してプログラムはクラッシュします。
重要なのは、panicの連鎖の途中でもdeferは実行されるという点です。
以下の例を実行してみます。
package main
import "fmt"
func main() {
defer fmt.Println("main関数のdeferが実行されました。")
doSomething()
fmt.Println("doSomething()の後ろの行。ここには到達しません。")
}
func doSomething() {
defer fmt.Println("doSomethingのdeferが実行されました。")
panic("緊急事態発生!")
fmt.Println("panicの後ろの行。ここには到達しません。")
}
実行結果は以下のようになります。
doSomethingのdeferが実行されました。
main関数のdeferが実行されました。
panic: 緊急事態発生!
goroutine 1 [running]:
main.doSomething()
/path/to/your/file.go:15 +0x...
main.main()
/path/to/your/file.go:7 +0x...
exit status 2
panicが発生した後も、各階層のdeferがきちんと実行されてからプログラムがクラッシュしていることがわかります。
個人的には、実行ログを見る限り、main関数のdeferも実行されているとなると、よほどでない限り使用を避けた方がいいかなと感じています。
defer とは?
deferは、関数の終了時に実行したい処理を記述しておくことができます。
-
defer文が実行された時点ではなく、deferを含む関数が終了する直前(returnする、またはpanicで抜ける直前)に実行されます。 - 複数の
deferがある場合、後に登録したものから順に(LIFO: Last-In, First-Out)実行されます。
deferの最も代表的な使い方が、ファイルなどのリソースの解放処理です。
func processFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
// ファイルを開いたら、すぐに閉じる処理を予約する
defer f.Close()
// ... ファイルを使った様々な処理 ...
// この関数のどこでreturnしても、f.Close()は必ず呼ばれる
return nil
}
deferを使うことで、関数の途中でエラーが発生してreturnした場合でも、ファイルのクローズ処理が実行されることを保証できます。
これにより、リソースの解放漏れを確実に防ぐことができます。
おわりに
今回は、Goのpanic, log.Fatal, deferの違いと使い分けについて解説しました。
これらの挙動を正確に理解することで、エラー処理だけでなく、より読みやすいコードを書くことができるようになると思います。
混同しやすい部分を整理整頓できてよかったです。
最後までお読みいただき、ありがとうございました!
この「学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025」では、GoやJavaを使い、APIの作り方、データベースとの接続、テストやDockerといった気になったバックエンド技術の基本を振り返った学びを共有しています。
ぜひ他の記事もチェックして、筆者がこのひとりアドカレを完遂することができるか、確認してみてください(^^)
学生ひよこ界隈が送るGo/Javaで実現する「はじめてのバックエンド」Advent Calendar 2025
参考文献