My External Storage

Jun 30, 2021 - 3 minute read - Comments - go

Goでプログラムを終了するときのお作法

Goにはいくつかプログラムを終了させる手段が存在する。
プログラムを終了させるときにどれを選べばいいか調べてみた。結論から言うとmain関数内でdeferを使わずos.Exit関数を呼ぶ。

TL;DR

  • Goで意図的にプログラムを終了させることができる処理は次のとおり
  • main関数内でdeferを使わずos.Exit関数を呼ぶのが一番良いと思う
  • panic関数は終了コードを明示的に決めることができない
    • 一方ですべてのゴルーチンを停止してくれるらしい
  • runtime#Goexit関数をmain関数で呼ぶのは行儀が悪そう
    • Calling Goexit from the main goroutine terminates that goroutine without func main returning.

    • defer宣言済みの関数に関してはすべて実行してから終了してくれるらしい
  • os#Exit関数は任意の終了コードでプログラムをただちに終了することができる
    • defer宣言済みの関数は実行されない

なお、すでにまとめている方のブログ記事も存在する。

また、この記事の調査内容は2021年6月時点で最新のGo1.16時点の各仕様である。

Goのプログラムを終了させる

私はGoのプログラムを終了させるときはいつもos#Exit関数を使っていた。

deferちゃんと動くしpanicじゃだめなんですか?」という質問を受けたときちゃんと答えられなかったので仕様を調査してみた。

Goでプログラムを終了させる方法

main関数をそのまま終了する、セグフォなどで異常終了する、などを除いて、コードの表現としてプログラムを終了させる方法は主に3つある。

  • os#Exit関数で終了する
  • panic関数で終了する
  • runtime#GoExit関数で終了する

それぞれの仕様を確認してみた。

os#Exit関数で終了する

一番オーソドックスな方法だと思っているが、仕様をみると、defer済みの関数については実行されないと記載があった。

The program terminates immediately; deferred functions are not run.

なので、main関数でdefer文と併用してos#Exit関数を実行すると意図しない挙動をする可能性がある。また、「immediately」なので他のゴルーチンの終了などは待機してくれない。

panic関数で終了する

調査前はpanic関数で終了するほうがめちゃくちゃな終了をすると思っていた。 しかし仕様を呼んでみると意外とdefer済みの関数などはちゃんと呼ばれるようだ。

When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller.

しかし、終了コードをプログラマが指定することはできない。

runtime#GoExit関数で終了する

直接使ったことはないが、runtime#GoExit関数でもプログラムを終了することができる。testingパッケージのFatal関数を呼ぶと内部で実行されるようだ。

https://github.com/golang/go/blob/go1.16.5/src/testing/testing.go#L710-L742

仕様を読むと、main関数で呼ぶ場合、怪しい挙動をするようなのでこれはプログラムを終了させるときに使わないほうがよさそうだ。

Calling Goexit from the main goroutine terminates that goroutine without func main returning. Since func main has not returned, the program continues execution of other goroutines. If all other goroutines exit, the program crashes.

結論

deferの利用に注意しながらmain関数でos#Exit関数を実行するのがよさそうだった。
コマンドラインならばたとえばこんな感じでよいのではないだろうか。

func main() {
  os.Exit(run(os.Args, os.Stdin, os.Stdout, os.Stderr))
}

func run(args []string, inStream io.Reader, outStream, errStream io.Writer) int {
    // do anything...
}

終わりに(余談)

panicのような予約語の機能の説明はどこを見ればよいのかわかっていなかった。
builtinパッケージを見るとpanicを始め、capappendと言ったビルドイン関数の明文化された仕様を読むことができるのを知れてよかった。

参考

関連記事