Carpe Diem

備忘録

goroutineでのpanicのハンドリング その2

概要

christina04.hatenablog.com

で書いていなかったことの追加です。

環境

recoverしたあとのエラーハンドリングをどうするか

panicをrecoverしたものはいいものの、

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()

    panic("panic")
}

The Go Playground

このように単純に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
}

The Go Playground

ここでのポイントは以下です。

  • 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
}

The Go Playground

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
}

The Go Playground

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
}

The Go Playground

汎用的な関数化

汎用的に使うのであれば、以下のような関数を用意すると良いでしょう。

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
    }

}

The Go Playground

複数の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のハンドリングがシンプルに書けますね。