問題
defer
を使って後処理をしたい場合に、プロセスがCtrl+cなどSIGINTで中断されるとdefer
したものが発火しない。プロセス自体が中断されるのであたりまえなんだけども。
問題の再現
package main import ( "fmt" "time" ) func main() { defer teardown() hoge() fmt.Println("終了") } func hoge() { for i := 0; i < 5; i++ { fmt.Println(i) time.Sleep(1 * time.Second) } } func teardown() { fmt.Println("データのあとかたづけ") }
上記の実行
% go run main.go 0 1 2 3 4 終了 データのあとかたづけ %
SIGINTの場合
% go run main.go 0 1 2 3 ^Csignal: interrupt % # 「データのあとかたづけ」が発火しない
SIGINTの捕捉
package main import ( "fmt" "os" "os/signal" "time" ) func main() { defer teardown() // {{{ ここから // SIGINTを待ち受けるchanを作成 c := make(chan os.Signal, 1) // それを登録 signal.Notify(c, os.Interrupt) // chanからの通知を受けるgoroutineを起動 go func() { for sig := range c { fmt.Println("シグナル来た", sig) close(c) // SIGINTをchanで吸収しちゃってるので、 // 明示的にExitする必要がある。 os.Exit(130) } }() // }}} ここまで追加 hoge() fmt.Println("終了") } func hoge() { for i := 0; i < 5; i++ { fmt.Println(i) time.Sleep(1 * time.Second) } } func teardown() { fmt.Println("データのあとかたづけ") }
os.Exit
を使い明示的にプロセスを終了しており、このままではdefer
したteardown
は呼ばれないので、以下の行を追加した。
for sig := range c {
fmt.Println("シグナル来た", sig)
close(c)
+ teardown()
// SIGINTをchanで吸収しちゃってるので、
// 明示的にExitする必要がある。
os.Exit(130)
実行してCtrl+cすると、
% go run main.go 0 1 2 ^Cシグナル来た interrupt データのあとかたづけ exit status 130 %
となっていい感じ。
defer
のためのリファクタリング
teardown
の呼び出しが2箇所に書かなきゃいけないのはあまり筋がよくないので、defer
一発でどうにかしたほうがシュッとすると思う。「hogeが終わった」というメッセージを伝達するdone
というようなチャンネルを経由して、SIGINTを受けるチャンネルと並列で扱うのがいいんではないか。終了コードが0
になってしまうが、まあいっか。
package main import ( "fmt" "os" "os/signal" "time" ) func main() { defer teardown() c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) done := make(chan error, 1) go hoge(done) select { case sig := <-c: fmt.Println("シグナル来た:", sig) /* teardown中に再度SIGINTが来る場合を考慮し、 send on closed channelのpanicを避ける。 */ // close(c) return case err := <-done: fmt.Println("hogeの終了:", err) } fmt.Println("終了") } func hoge(done chan<- error) { for i := 0; i < 5; i++ { fmt.Println(i) time.Sleep(1 * time.Second) } done <- nil close(done) } func teardown() { fmt.Println("データのあとかたづけ") }
非常にGoっぽいコードになった。
参考
- Exit Codes With Special Meanings
- How to exit a go program honoring deferred calls? - Stack Overflow
- 本論ではないが、関係tips。panicをhackしている。あんまりお行儀はよい感じはしない。
runtime.Goexit
使ったことないけど、使わないなら使わないに越したことはない雰囲気を感じた。どうなんだろ。
- 本論ではないが、関係tips。panicをhackしている。あんまりお行儀はよい感じはしない。
- Handling CTRL-C (interrupt signal) in Golang Programs | I care, I share, I'm Nathan LeClaire.
- Delete machines even when the process get interrupted intentionally · Issue #31 · otiai10/awsub · GitHub
- postgresql - What does exit code 130 mean for postgres command? - Unix & Linux Stack Exchange
- Exit with error code in go? - Stack Overflow
DRYな備忘録として
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- 作者: Alan A.A. Donovan,Brian W. Kernighan,柴田芳樹
- 出版社/メーカー: 丸善出版
- 発売日: 2016/06/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る