この記事は 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 }
このとき,panic
後 recover
されたらこの関数の戻り値はどうなるのか?
やってみた.
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 }
いや,そもそも defer
で return
を呼ぶなんて無理か…….
ドキュメントを探すと,やり方はあるらしい.
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 }
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
されると,error
は nil
で返さる.エラーが起こっているにもかかわらず,以下のようなエラーチェックをしていると(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