Goのエラー処理② panicとrecover
Goのpanicとrecover
Goのエラー処理は原則として例外を使わず、エラーを丁寧に処理すべき方針はわかったとして、Goの例外機構というものがどういう仕組なのか調べた。
まず、基本に忠実に、golang.jpの説明を読むと、
Recover
panicが呼び出されたとき(これには、配列のインデックスが範囲外であるときや、型アサーションに失敗したような暗黙的なものも含む)は、すぐさま、カレントの関数を停止し、ゴルーチンのスタックの巻き戻しを開始します。その途中、遅延指定されている関数をすべて実行します。この巻き戻しがゴルーチンのスタックの先頭にたどり着くと、プログラムは終了します。ただし、組み込み関数recoverを使うことで、ゴルーチンの制御を取り戻し、通常の実行を再開させることが可能です。
panicが発生すると即座に処理を打ち切るので、これが他の言語の例外に相当するものらしい。
「その途中、遅延指定されている関数をすべて実行します」とも言っているので、deferに無名関数を指定してrecoverしないとダメなんじゃないかと思ったら、ご丁寧にその後に明記されていた。
recoverを呼び出すと、巻き戻しを停止し、panicに渡された引数が返ります。巻き戻り中に実行できるコードは、defferで遅延指定された関数内のみなので、recoverは遅延指定された関数内でのみ役立ちます。
Pythonとの比較
例えば、リトライ可能な例外がサブルーチンから送出された場合は例外を握りつぶし、それ以外は上位ルーチンに送出するプログラムをPythonで書くとこんな感じだろう。
class Retryable(Exception): pass try: do_something() // 何か例外を送出する可能性がある処理 except Retryable as ex: print(ex) except Exception as ex: raise ex
普通にtry-exceptブロック使うでしょう。
対して、Goで似たようなことをやろうとすると、こんな感じだろう。
type Retryable struct{} defer func() { switch p := recover(); p { case nil: // ここはパニックが起きなかった場合が該当する fmt.Println("No error.") case Retryable{}: err = fmt.Errorf("failed, retry") // err = errors.New("failed, retry") default: panic(p) } }()
Pythonがブロックを扱う方法を採るのに対し、Goはブロックを監視する仕組みではなくあくまでも関数ベースの解決方法を採ることがよく分かる。
「実践Go言語」のページにはこのようにも書いてある。
他言語のブロックレベルのリソース管理に慣れたプログラマには、deferは特異に見えるかも知れませんが、deferのもっとも面白く、強力な活用法は、deferがブロックベースではなく、関数ベースであることによって生み出されます。panicとrecoverのセクションに、この可能性を示す例があります。
やっぱり、recoverはdeferと一緒に使わないと意味がないみたいだ。
また、Pythonのコンテキストマネージャはまさにブロックを監視する仕組みなので、Goで同じことをしようと思ったらdeferを使うのだろう。
とはいえ、Pythonでコンテキストマネージャを作るときはcontextlib.contextmanagerで関数をラップすれば簡単に作れるので、Goの流儀でPythonのコンテキストマネージャを考えると、Pythonには関数ベースの基本的なアプローチの上に、ブロックベースの仕組みに置き換える方法が用意されていると捉えるべきか(謎)。
contextlibを使う場合はこんな感じ。
from contextlib import contextmanager class Retryable(Exception): pass @contextmanager def retrying(): try: yield except Retryable as ex: print(ex) # except Exception as ex: # raise ex with retrying(): do_something() // 何か例外を送出する可能性がある処理
実際に試した
せっかくなので、エラーを返却する原則的なやり方と、panic/recoverを利用する方法をそれぞれ使い分けるサンプルコードを書いてみた(以下)。
package main import "fmt" type Challenger struct { count int threshold int } type Retryable struct{} // 非公開メソッドの方はわざとpanicを利用する。 func (c *Challenger) attack() bool { if c.count < c.threshold { c.count++ panic(Retryable{}) // パニック、しかしリトライ可能 } if c.count > c.threshold { panic("No more chance.") } c.count++ return true } // 公開メソッドはpanicを捕捉してリカバーをかける。 func (c *Challenger) Attack() (ok bool, err error) { defer func() { switch p := recover(); p { case nil: // ここはパニックが起きなかった場合が該当する fmt.Println("No error.") case Retryable{}: err = fmt.Errorf("Failed(%v times)", c.count) default: panic(p) } }() ok = c.attack() return } func main() { var c *Challenger c = &Challenger{0, 3} fmt.Println(c.Attack()) // false Failed(1 times) fmt.Println(c.Attack()) // false Failed(2 times) fmt.Println(c.Attack()) // false Failed(3 times) fmt.Println(c.Attack()) // true <nil> fmt.Println(c.Attack()) // 以下panic // panic: No more chance. [recovered] // panic: No more chance. // // goroutine 1 [running]: // panic(0x4b9f40, 0xc82000a400) // /usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6 // main.(*Challenger).Attack.func1(0xc820039e30, 0xc820039e58) // /home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:35 +0x2fc // panic(0x4b9f40, 0xc82000a400) // /usr/lib/go-1.6/src/runtime/panic.go:443 +0x4e9 // main.(*Challenger).attack(0xc820039e58, 0x52c678) // /home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:19 +0xd0 // main.(*Challenger).Attack(0xc820039e58, 0x0, 0x0, 0x0) // /home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:38 +0x78 // main.main() // /home/echizen/_go/src/github.com/oyakata/tokyo/trash/retry.go:49 +0x52c // exit status 2 }