はじめに
本記事における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")
}
main
がSomeFunc
を呼んで、SomeFunc
がpanic
を呼んでいます。これを実行すると以下の出力が得られます(ユーザー名は隠してあります)。
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
を使いたくなりますが、それでは他のテストにまで影響が及んでしまいます。このあたりは使い分けが必要でしょう。