【Golang】Exit, panic, Goexitの違い

2019-02-17 programming golang

Go言語のプログラムにおいて, 例外的な事態が発生したときに呼ばれる関数は, 代表的なものが3つあります. それはExitpanicGoexitです. この記事ではこれらの違いを説明します.

はじめに

本記事における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を使いたくなりますが, それでは他のテストにまで影響が及んでしまいます. このあたりは使い分けが必要でしょう.