概要
で書いていなかったことの追加です。
環境
- golang 1.11.4
recoverしたあとのエラーハンドリングをどうするか
panicをrecoverしたものはいいものの、
func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() panic("panic") }
このように単純にrecoverしてハイ終わり、としているとpanicを起こしたのに処理が正常系で進んでしまうという問題があります。
特にWebサーバとしてはpanicが起きたので500エラーとして返すべきです。
なのでpanicをerrorに変換して、後の処理にerrorを渡す必要があります。
ポイント
単純にdeferの中でerror返せばいいじゃん、ということはできません。
例えば以下のコードはコンパイルすら通りません。
func f() error { defer func() { if r := recover(); r != nil { return r } return nil }() panic("some panic") return nil }
ここでのポイントは以下です。
- defer内でpanicをerrorに変換する
- named return valuesにする
defer内でpanicをerrorに変換する
そもそもrecover()
が返すのはinterface{}
であるので、error型でないためコンパイルが通りません。
なので以下のようにerror型に変換してあげましょう。
defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %s", r) } }()
named return valuesにする
公式ドキュメントにあるように、
Deferred functions may read and assign to the returning function's named return values.
Defer, Panic, and Recover - The Go Blog
Deferred functionsが戻り値を上書きできるのはnamed return valuesのみです。
なので以下の2つのやり方ではできません。
NG:スコープであらかじめ定義するだけ
func f() error { var err error defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %s", r) } }() panic("some panic") return err }
NG:deferの引数に渡す
func f() error { var err error defer func(e error) { if r := recover(); r != nil { e = fmt.Errorf("panic recovered: %s", r) } }(err) panic("some panic") return err }
OK:named return valuesにする
このようにすればちゃんと上書きできます。
func f() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %s", r) } }() panic("some panic") return }
汎用的な関数化
汎用的に使うのであれば、以下のような関数を用意すると良いでしょう。
func recoverer(f func()) func() error { return func() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %v", r) } }() f() return } }
複数のgoroutineの場合どうするか
複数のgoroutineを扱うのであれば、sync/errgroupを使うのが良いです。
sync/errgroupを使った例
func run() error { eg := errgroup.Group{} for i := 0; i < 10; i++ { i := i eg.Go(func() error { return worker(i) }) } if err := eg.Wait(); err != nil { return err } return nil }
このように
Go(func() error)
で新しいgoroutineとして実行し、Wait() error
でgoroutineの完了を待ってエラーを受け取ります。
Wait()
が戻すエラーはGo()
内で最初に返されたエラーです。
sync.WaitGroupでやることもできますが、エラー用のchannelを用意したりとやや手間です。
なのでsync/errgroup
をオススメします。
前述のrecover関数と組み合わせてみる
errgroupと先ほどの汎用的なrecover関数を組み合わせることで
func run() error { eg := errgroup.Group{} for i := 0; i < 10; i++ { i := i eg.Go(recoverer(func() { worker(i) })) } if err := eg.Wait(); err != nil { return err } return nil } func worker(i int) { fmt.Println(i) if i > 5 { panic(i) } }
このように別goroutineでもpanicを拾ってエラーとして返す事が可能になります。
複数のgoroutineを扱うケースでもpanicのハンドリングがシンプルに書けますね。