go言語でシグナルをきちんとエラーハンドリングする
関連
並行処理全般に関するメモは以下 go言語の並行処理 - ludwig125のブログ
go言語でsignalを適切に処理する方法を調べたので例をいくつか
シグナルを受け付けて関数を適切に終了させる例1
SIGINT, SIGTERMを受け付けられるsignalの例
参考:
package main import ( "context" "fmt" "os" "os/signal" "syscall" "time" ) func main() { sigs := make(chan os.Signal, 1) ctx, cancel := context.WithCancel(context.Background()) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) defer func() { // シグナルの受付を終了する signal.Stop(sigs) cancel() }() go func() { select { case sig := <-sigs: // シグナルを受け取ったらここに入る fmt.Println("Got signal!", sig) cancel() // cancelを呼び出して全ての処理を終了させる } }() if err := doTask(ctx); err != nil { fmt.Printf("failed to doTask: %v", err) cancel() // 何らかのエラーが発生した場合、他の処理も全てcancelさせる return } fmt.Println("done successfully.") } func doTask(ctx context.Context) error { defer fmt.Println("done doTask") for i := 0; i < 5; i++ { select { case <-ctx.Done(): fmt.Println("received done") return ctx.Err() default: } // // エラー時の挙動が見たい場合はここのコメントアウトを外す // if i == 3 { // return fmt.Errorf("error happened") // } // do something fmt.Println("sleep 1. count:", i) time.Sleep(1 * time.Second) } return nil }
動作確認
順にそれぞれの挙動を見てみる
正常終了時
$go run signal3/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 sleep 1. count: 3 sleep 1. count: 4 done doTask done successfully.
異常終了時(上のコメントアウトを外した時)
$go run signal3/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 done doTask failed to doTask: error happened%
Ctrl+Cをした時
$go run signal3/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 ^CGot signal! interrupt received done done doTask failed to doTask: context canceled%
- Ctrl+Cのとき、signalを受け取った後cancelされていることがわかる
シグナルを受け付けて関数を適切に終了させる例2(タスクの中身がgoroutine)
練習がてら、doTaskの中身をgoroutineにしてみた場合も考えてみた
package main import ( "fmt" "os" "os/signal" "time" "golang.org/x/net/context" ) func main() { ctx, cancel := context.WithCancel(context.Background()) sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt) defer func() { // シグナルの受付を終了する signal.Stop(sigs) cancel() }() go func() { select { case sig := <-sigs: // シグナルを受け取ったらここに入る fmt.Println("Got signal!", sig) cancel() // cancelを呼び出して全ての処理を終了させる return } }() res, err := doTask(ctx) for v := range res { fmt.Println("done successfully.", v) } for e := range err { fmt.Printf("failed to doTask: %v", e) cancel() // 何らかのエラーが発生した場合、他の処理も全てcancelさせる return } } func doTask(ctx context.Context) (<-chan string, <-chan error) { resCh := make(chan string) errCh := make(chan error, 5) go func() { defer fmt.Println("done doTask") defer close(resCh) defer close(errCh) for i := 0; i < 5; i++ { select { case <-ctx.Done(): fmt.Println("received done") // Do something before terminated time.Sleep(500 * time.Millisecond) errCh <- ctx.Err() return default: } // // エラー時の挙動が見たい場合はここのコメントアウトを外す // if i == 3 { // errCh <- fmt.Errorf("error happened") // return // } // do something fmt.Println("sleep 1. count:", i) time.Sleep(time.Second) } resCh <- fmt.Sprintf("something") }() return resCh, errCh }
動作確認
正常終了時
$go run signal5/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 sleep 1. count: 3 sleep 1. count: 4 done doTask done successfully. something
異常終了時(上のコメントアウトを外した時)
$go run signal5/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 done doTask failed to doTask: error happened%
Ctrl+Cをした時
$go run signal5/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 ^CGot signal! interrupt received done done doTask failed to doTask: context canceled%
問題なさそう
シグナルを受け付けて関数を適切に終了させる例3(独自のcontextを用意する)
こちらの記事で紹介されていたNewCtxを使ってみる Managing Groups of Goroutines in Go - The Startup - Medium
これは、signalを処理してcontextのcancelを送るctxを定義するもの
シグナルを受け付けるまで処理し続けるような場合(main側でエラー時にcancelを使う必要がない場合)では、上のコードがかなり減った
package main import ( "fmt" "os" "os/signal" "syscall" "time" "golang.org/x/net/context" ) func main() { res, err := doTask(newCtx()) for v := range res { fmt.Println("done successfully.", v) } for e := range err { fmt.Printf("failed to doTask: %v", e) return } } func newCtx() context.Context { ctx, cancel := context.WithCancel(context.Background()) go func() { sCh := make(chan os.Signal, 1) signal.Notify(sCh, syscall.SIGINT, syscall.SIGTERM) <-sCh fmt.Println("Got signal!") cancel() }() return ctx } func doTask(ctx context.Context) (<-chan string, <-chan error) { resCh := make(chan string) errCh := make(chan error, 5) go func() { defer fmt.Println("done doTask") defer close(resCh) defer close(errCh) for i := 0; i < 5; i++ { select { case <-ctx.Done(): fmt.Println("received done") // Do something before terminated time.Sleep(500 * time.Millisecond) errCh <- ctx.Err() return default: } // do something fmt.Println("sleep 1. count:", i) time.Sleep(time.Second) } resCh <- fmt.Sprintf("something") }() return resCh, errCh }
実行結果
シグナル送った時
$go run signal_pattern/signal6/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 ^CGot signal! received done done doTask failed to doTask: context canceled
正常終了時
$go run signal_pattern/signal6/signal.go sleep 1. count: 0 sleep 1. count: 1 sleep 1. count: 2 sleep 1. count: 3 sleep 1. count: 4 done doTask done successfully. something