golangでrecoverしたときの戻り値

この記事は Crowdworks Advent Calendar 2015 13日目の記事になります. クラウドワークスの業務では全然goを使っていないけど,goの話をします.

panicとrecover

goには例外がないと言われているし,そもそもあんまり panic を使う機会はないと思う.もし頻繁に panic していたら,おそらくその panic の使い方,goではあまり推奨されることじゃないので「本当にそれpanic必要なの?」と疑ったほうがいい. golangのpanicは例外ではないのか?

そして,さらにはrecover というものがあり,こいつは panic が起きた時の処理をハンドリングできる.

func hoge() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("recoverd!\n")
            // Do something
        }
    }    
    panic("hogehoge")
}

としておくと,hoge()panic に陥らない.

できれば,あまり panic を使わないで欲しいし,ましてや recover なんてもっと使わないでほしい. recover については以下の記事が詳しい. panicはともかくrecoverに使いどころはほとんどない

わかっていても使わなきゃいけない場合もある

直近で困ったのは,sql パッケージを使った時のトランザクションだった. トランザクションなので,panic が発生した場合は Rollback してもらわないと困る.

func hoge(tx *sql.Tx) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
        }
    }()
    // Do something
    tx.Exec(…)
}

[Go言語] database/sqlパッケージを使ってみた

もちろん,SQLの実行結果としてエラーを受け取れる場合は多々ある.

err := tx.QueryRow("SELECT name FROM users WHERE id = ?;", id).Scan(&name)
if err != nil {
    tx.Rollback()
}

その場合は,ちゃんと Rollback を呼びだせばいいのだけれど,その他のコードで panic が呼ばれる可能性は否定できない.

そういった場合でもちゃんとトランザクションロールバックしてほしい.というわけでどうしても recover せざるを得ない.

recoverしたときの戻り値どうなるの?

戻り値を宣言しているにもかかわらず,return しなかった場合には,ふつうコンパイラに怒られる.では,recover したときは return しなくていいのだろうか? 一応コンパイラからは怒られていない.では、一体どんな値が返ってくるんだろうか?

boolの戻り値

以下のように関数を定義していたとする.

func hoge() bool {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("recoverd!\n")
        }
    }()
    // Do something
    return true
}

このとき,panicrecover されたらこの関数の戻り値はどうなるのか? やってみた.

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: false
}

func rescue() bool {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)  // => panic: error
        }
    }()

    panic("error")

    return true
}

へー,false になるらしい.

intの場合は?

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: 0
}

func rescue() int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)  // => panic: error
        }
    }()

    panic("error")

    return 10
}

つまり定義時の値を返している

上記のような例の場合,処理の流れ的にどこにも return する場所がないまま,panic / recover になってしまっている. そのときにどうやら,その型で定義したときの値を返している様子.

var i int    // => 0
var s string // => ""
var b bool   // => false

というわけで,recover で拾う場合はこのことを覚えておきましょう.何も考えずにrecover していると,うっかり正常に終了しているように見えるような値を返しているかもしれない.

そして例外的に使っているからこそ,できるだけ何が起こったかは把握しておいた方が良くて,recover した時にログくらい出しておかないとわからなくなる.

recover時に戻り値を指定する

defer 内で return しようとすると……

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: false
}

func rescue() bool {
    defer func() bool {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)    // => panic: error
            return true
        }
        return true
    }()
    return true
}

特に考慮はしてくれないらしい.というか,defer で呼び出した関数の戻り値は,親の関数の戻り値と何ら関係ないのか……. さすがに以下のようなことはできない

func rescue() bool {
    defer return func() bool {    // => syntax error: unexpected return
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)
            return true
        }
        return true
    }()
    return true
}

いや,そもそも deferreturn を呼ぶなんて無理か…….

ドキュメントを探すと,やり方はあるらしい.

func main() {
    fmt.Printf("rescue return: %+v\n", rescue())  // => rescue return: true
}

func rescue() (b bool) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panic: %+v\n", err)
            b = true
        }
    }()

    panic("error")    // => panic: error

    return true
}

Defer, Panic, and Recover

Deferred functions may read and assign to the returning function's named return values.

なるほど,return はできないけど,return の値を書き換えることはできると…….

returnの書き換えを使うパターン

先のトランザクションの例で,例えば以下のようにちゃんと error を返すコードを書いていたとする.

func hoge(tx *sql.Tx) (bool, error) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
        }
    }()
    // Do something
    tx.Exec(…)
    return true, nil
}

この場合,recover されると,errornil で返さる.エラーが起こっているにもかかわらず,以下のようなエラーチェックをしていると(goではすごく一般的なエラーチェックコード),呼び出し元でうまいことエラーチェックできない可能性がある.

func main() {
    result, err := hoge(tx)
    if err != nil {
        log.Fatal("error: ", err.Error())
    }
    // Do something
}

こんなコードを書いていたら,全然ハンドリングされないかもしれない. なので,error はやはりきっちり返したい.というようなときに,先ほどのreturn値の書き換えを使うしかなくなる.

func hoge(tx *sql.Tx) (r bool, e error) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
            r = false
            e = errors.New("unexpected error!")
        }
    }()
    // Do something
    tx.Exec(…)
    return true, nil
}

っていうかできるだけrecover使わないで書こう

上記のような例は仕方ないにしても,できる限り,recover することや,その戻り値をアテにすることは避けたほうが良い.できることなら,goの一般的な関数らしく,戻り値にerror を含めて,何か起こったらpanic するのではなくerrors.New() した方がいい.

というのは,どこで聞いても言われることらしい. http://stackoverflow.com/questions/19934641/golang-returning-from-defer