Goのlog.Fatalはos.Exit(1)を呼ぶ——deferが実行されない理由と使い分け

6分で読めるテック
最終更新:

あるAPIクライアントのエラー処理に log.Fatal(err) を書いたところ、後続の処理が一切動かなくなりました。defer で書いたクリーンアップ処理も実行されず、ログを確認してようやく「プログラムが終了している」と気づきました。

同じところでつまずかないように、log.Fatal がプログラムを終了させる理由と、ログ関数4種の使い分けを整理しておきます。

なぜlog.Fatalでプログラムが終了するのか

Goの公式ドキュメントには次のように書かれています。

Fatal is equivalent to Print() followed by a call to os.Exit(1).

つまり log.Fatal は「メッセージを出力してから os.Exit(1) を呼ぶ」関数です。os.Exit が呼ばれるとGoroutineのスタックアンワインドが行われないため、defer で登録した関数も実行されません。

自分が実装していたコードは次のようなものでした。

items, err := client.SearchItems(categoryId)
if err != nil {
    log.Fatal(err)
}

API呼び出しでエラーが返ってきた瞬間にプログラムが終了するため、他の categoryId の処理には到達しないまま終わっていました。

log.Fatalfl系列の亜種

log.Fatal 以外にも os.Exit を呼ぶ系列が2つあります。

  • log.Fatalf(format, v...)Printf 相当のフォーマット付き出力後に os.Exit(1)
  • log.Fatalln(v...)Println 相当の改行付き出力後に os.Exit(1)

どれも最終的に os.Exit(1) を呼ぶ点は同じです。log.Panic / log.Panicf / log.Paniclnpanic を起こすため deferrecover できますが、Fatal 系は recover 不可能な点に注意が必要です。

ログ関数4種の比較

Goでエラーをログする場面では主に4種類の選択肢があります。

関数内部動作defer実行用途
log.Printメッセージ出力のみされるエラーをログして処理継続
log.FatalPrint + os.Exit(1)されない回復不能なエラーで即終了
panicスタックアンワインドされる回復可能な異常(recover で捕捉できる)
os.Exitプロセス即終了されない明示的にプロセスを終わらせたい場合

panicdefer された recover() で捕捉できますが、log.Fatalos.Exit は捕捉できません。

os.Exit を直接呼ぶケースは、終了コードを細かく制御したいときです。log.Fatal は常に os.Exit(1) を返しますが、os.Exit(0) で正常終了を明示したり、特定の終了コードをシェルスクリプトに返したい場合は os.Exit を直接使います。

処理を継続したい場合はlog.Printを使う

今回は全カテゴリを一度回し切りたいという要件があったため、1件のAPIエラーで全体が止まってはいけませんでした。log.Print に変更することで、エラー内容はログに残しつつ処理を継続できるようになりました。

items, err := client.SearchItems(categoryId)
if err != nil {
    log.Print(err) // エラーをログして次の categoryId の処理に進む
    return
}

return で関数を抜けることで、呼び出し元のループが次の要素に進みます。

Go 1.13以降はエラーラッピングを使うと呼び出し元での識別が楽になります。

if err != nil {
    log.Printf("SearchItems categoryId=%d: %v", categoryId, err)
    return
}

エラーメッセージに categoryId を含めておくと、ログを後から追跡するときにどの処理で失敗したか一目でわかります。

log.Fatalを使う場面

log.Fatal が適切なのは、続行しても意味がない初期化エラーのような場面です。たとえばデータベース接続やコンフィグ読み込みの失敗は、そのまま起動しても壊れた状態で動き続けるだけなので、即終了が妥当です。

db, err := sql.Open("postgres", dsn)
if err != nil {
    log.Fatal(err) // DB接続失敗は回復不能なので即終了が適切
}

main 関数や初期化フェーズに限って log.Fatal を使い、それ以外では log.Print でエラーを記録して呼び出し元に error を返す設計が、Goらしいエラーハンドリングのパターンです。ライブラリ内で log.Fatal を呼ぶと、そのライブラリを使う側がプロセス終了を防げなくなるため、ライブラリ内での使用は避けるべきとされています。

APIエラーのように「1件失敗しても他の件は続けられる」場合は log.Print 、「この失敗があったら動き続けても意味がない」場合は log.Fatal という判断基準が自分には合っています。

質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。

このサイトについて

井上 周(Amane Inoue)の個人ブログです。技術・読書・ドラマ・旅・大学生活のことを書いています。