🌟

log.Fatalは使わないようにしよう

2023/05/18に公開

Goの超基本的なプラクティスとしてlog.Fatalは使ってはいけないということがありますが、全然広まってないし、初心者の人はカジュアルに使いがちだったりするので、注意喚起のために記事にします。

まず、なぜlog.Fatalを使ってはいけないかですが、log.Fatalを呼ぶと中でos.Exitが呼ばれてプログラムが強制終了されるからです。

プログラムが強制終了されて何がまずいかというと、通常Goではdeferで後処理を予約するようなコードを多用しますが強制終了してしまうとこれらの後処理が呼ばれずリソースの適切な解放などが行われないまま終了してしまいます。

またサーバーアプリケーションで例え処理を中断したいというだけの意図でlog.Fatalを呼んだとしてもサーバープロセス全体が落ちてしまいます。サーバー以外でもgoroutineで並列処理しているようなプログラムでlog.Fatalを呼び出した場合、他のgoroutineごとまとめて落とされてしまいます。

このように見た目以上に危険な処理になっているのがlog.Fatalです。

分かって使う分にはいいじゃないかと思う人もいるでしょうが、以下の理由でそれも止めた方がいいです。

  1. log.Fatalの字面から純粋にログ系の関数に見えるため、呼び出してはいけないケースで使われていたとしてもレビューなどで気付きづらい
  2. 書いたときは強制終了しても問題ないユースケースだったとしても、後から強制終了してはまずいケースに転用される可能性がある

2については実体験として、書き捨てのバッチプログラムで使われていたコードが後から重要なサーバーアプリケーションに転用され、log.Fatalも混入してしまったというケースがあります。実際気づいたときには青ざめました。

1で挙げたようにlog系の関数として捉えられてしまいがちなので、意識してないと見落としてしまいがちな点も危険だと考えています。もしプログラムの処理として強制終了を入れたいのであれば、明示的にos.Exitを呼ぶ方が気づきやすいでしょう。

ですので最初からlog.Fatalは完全に禁止してしまったほうがいいと思っています。

ではどういうふうに書くのが正解なのでしょうか?

私がよく書くのは以下のようなコードです。

func run() error {
    // 処理本体
}

func main() {
    err := run()
    if err != nil {
        log.Fatal(err)
    }
}

errorを返す関数を用意して、従来mainに入れていたような処理本体のコードをそちらに移し、エラーで中断する場合は代わりにエラーをリターンするようにします。main関数はエラーを受け取りnilでなければlog.Fatalを呼び出します。

そしてそれ以外のコードではlog.Fatalは使わないようにコーディングルールなどで強制します。
Linterのforbidigoで弾いてしまってもいいかもしれません(golangci-lintにも入っています)。

このようにすればトップレベルで1度しか呼び出されないため安全にlog.Fatalを使うことが出来ます。

また上記のようなコードを書いても大して手間ではないと思いますが、書き捨てのコードなどでより手抜きしたいという場合はlog.Panicを使うのもおすすめです。panicならdeferを待たずに強制終了したりはしないからです。

とくに既存のコードでlog.Fatalを多用している場合に、errorを返すような形に書き換えるのが大変な場合はlog.Panicに置き換えることで雑に落とすコードのまま安全な形に書き換えることが出来ます。

どちらにせよlog.Fatalは一般的なコードでは使わないようにし、またチームで開発する際には危険な関数であることを周知することが重要です。

この記事が、log.Fatalは使ってはいけない関数だということが周知される一助になれば幸いです。

Discussion