【Golang】Exit, panic, Goexitの違い

2019-02-17

はじめに

本記事におけるGoのバージョンは1.11.5です。

関数定義

3つの関数定義を、パッケージとともに表示すると以下のようになります。

func os.Exit(code int)
func builtin.panic(v interface{})
func runtime.Goexit()

3つの違い

ざっくり言うと、上のほうが過激で、下のほうが穏やかです。

os.Exit

引数の整数は終了コードです。今回は例外的な事態が発生した場合を考えているので、終了コードとしては0以外を考えています。

プログラムの中でos.Exit(0以外)が呼ばれると、以下のような挙動をとります。

  • プログラムは直ちに終了する。
  • deferされた関数は呼ばれない。

builtin.panic

引数にはログ出力したい文字列等を指定します。したがってこんな使い方をすることが多いと思います(builtinパッケージはインポートする必要はありません)。

if err != nil {
    panic(err)
}

panicの挙動を説明するために、サンプルコードを用意しました。

# main.go

package main
import fmt

func main() {
    fmt.Print("inside main")
    defer fmt.Print("deferred function inside main")

    SomeFunc()
}

func SomeFunc() {
    fmt.Print("inside SomeFun")
    defer fmt.Print("deferred function inside SomeFunc")

    panic("panic!!!")

    defer fmt.Print("this function wouldn't be called")
}

mainSomeFuncを呼んで、SomeFuncpanicを呼んでいます。これを実行すると以下の出力が得られます(ユーザー名は隠してあります)。

inside main
inside SomeFun
deferred function inside SomeFunc
deferred function inside main
panic: panic!!!

goroutine 1 [running]:
main.SomeFunc()
        /Users/***/Repogitories/github.com/logicoffee/play_with_go/main.go:22 +0xd5
main.main()
        /Users/***/Repogitories/github.com/logicoffee/play_with_go/main.go:15 +0xbe
exit status 2

これを言葉で説明するとこうなります。

  • SomeFunc内でpanicが呼ばれると処理がストップする。
  • SomeFunc内でそれまでにdeferされた関数が順次処理される。
  • mainへと処理が戻る。
  • mainからすると、以下が起こったように見える。
func main() {
    fmt.Print("inside main")
    defer fmt.Print("deferred function inside main")

    panic("panic!!!")
}
  • したがってmain内の処理はストップし、それまでにdeferされた関数が順次処理される。
  • 最終的にはプログラム全体がストップし、panicに渡された引数が表示される。

runtime.Goexit

おおかたpanicと同じような挙動をとりますが、一番違うのは他のgoroutineの終了を待つ点です。

panicは他のgoroutineの終了を待たずにプログラム全体が終了します。

runtime.Goexitは他のgoroutineが終了するのを見届けてから終了します。

使い分け

そもそもこれら3つの関数を直接書くことは少なめかもしれません。しかし直接でなくとも裏で呼び出すケースはありえます。例えば以下の関数がそうです。

// logパッケージ
log.Fatal() //os.Exit(1)が呼ばれる
log.Panic() //panic()が呼ばれる

// testingパッケージ
T.Fatal() //runtime.Goexit()が呼ばれる

テスト内でlog.Fatal()を呼び出すと、他に控えているテストが実行されずに終了してしまいます。

テストの準備(テストデータの挿入とか)が失敗した場合にはlog.Fatalを使いたくなりますが、それでは他のテストにまで影響が及んでしまいます。このあたりは使い分けが必要でしょう。