読者です 読者をやめる 読者になる 読者になる

最近思うgolangのerror

golang プログラミング

golangが結構好きになりつつある. そんな中でも延々と悩まされ,設計を考え続けているのが,error型だ.

もしかしたら,あまりgolangmysqlのような,がっつりSQLでできているRDBを使う人は少ないのかもしれない.

だけど,自分が作っているものが,がっつりWebサービスで,どうしてもRDBに近いところで動かさなきゃいけないので,こういうことで悩む.

panic時にはtransactionのrollbackを

func hoge(tx *sql.Tx) (e error) {
    defer func() {
        if err := recover(); err != nil {
            tx.Rollback()
            e = errors.New("unexpected error")
        }
    }()
    tx.Exec(...)
}

どうしてもtransactionを使うとこういう処理を書かざるをえない. もちろん, sql.Txerror型を返すのだが,それ以外のところで,例えば nil 参照等が起こらない保証がない. どうしても,そういうリスクは消し去ることが出来ない以上,どこかでrollbackを仕掛けておくべきだろう.

というわけで,しばらくこの方式で,transaction周りではrecoverで防いでいたのだが,いざ本当にrecoverしなきゃいけないようなエラーが起こった時に,困ったことに気付いた.

unexpectedって何が起こったかわからない

当然だった. 自分で握りつぶしてunexpectedにしているんだから,何が起こったのか全然わからなかった.

ただ,recoverした段階ではerror 自体は取得できているはずだ.

というわけで,苦戦してみた.

func hoge(tx *sql.Tx) (e error) {
    defer func() {
        if err := recover(); err != nil {
            transaction.Rollback()
            switch ty := err.(type) {
            case runtime.Error:
                e = ty
            case string:
                e = errors.New(err.(string))
            default:
                e = errors.New("unexpected error")
            }
        }
    }()
}

なるほど,こうすれば,何が起こったのかをerrorとして渡すことができる.

まぁ本当はすべてのエラーを渡したいところではあるが,recoverした段階では err はただの interface{} になってしまっているので,実際どんな型にキャストできるかはわからない. なので仕方なく, runtime.Error と,任意の errors から出てくるエラーだけ拾っている. まぁ,大抵のものは拾えたりする.

どこでエラーになったのか?

これは今でも悩みどころだ.

error としてメソッドの戻り値にしてしまうのはとてもよかった. ただ,メソッド呼び出しが何階層も深いところで起こっていて,最終的に呼び出し元のメソッドerror 型が渡ってきた時,はたしてそのエラーが,どのメソッドで発生したものかを特定することができるだろうか?

これはなかなか難しい問題だ. もちろんすべてのエラーについて,たとえばメソッド名を頭につけるような変更をしておけばいい.

ただ,これはこれで書く人間にやたら負担を強いるし,必ずしもstringにキャストできるerrorばかりではないので,あまり現実的とは言い難い.

エラーの通知

また,エラーを通知することを考えると,いったいどの階層でエラーをハンドリングして通知すればいいのだろうか?

自作のメソッド,A, B, Cがあったとする.

A -> B -> C という階層順でメソッド呼び出しを行い,Cのどこかでエラーが発生したとしよう. error型の戻り値は,C -> B -> Aと伝搬する.

さぁ,果たしてエラーの通知はどこで行うべきだろうか?

Aでエラーの通知を行うと,Aから見た場合,「Bを呼び出したらエラーになった」という情報しか得られない. しかし,実際にエラーになったのはメソッドCの中である.

では,Bでエラー通知を行うとしよう.

そうすると,Bから見た場合「Cの呼び出しでエラーになった」という情報しか得られず,結局Cの中のどこがダメだったのかわからない.

最下層のメソッドだけでエラー通知をする

やはりここはCでエラー通知を行おう. 戻り値を返す手前でエラー通知してしまえばいいのだ.

たしかに一見正しい情報を伝えることができる.

しかしその後のことを考えよう. Cでエラー通知をしたから,AもBもエラー通知をしなくても良いのか?

こうしてしまうと,Cについては良くても,たとえば,A -> Dのようなメソッド呼び出しがあった場合,Aは常にDがメソッド内でエラー通知しているかを確認しなければならない. メソッドの呼び出しが単純ならばそれほど困らないが,いろんな場所で使いまわされるメソッドであったり,逆にいろんなメソッドを呼び出すようなメソッドを作る場合,信じられないくらいの負担になってくる.

また,自身が呼び出し順序的に最下層のメソッドになっているという判定はどうしたらできるだろうか? これは非常に難しい問題になってきてしまうので,結局最終的には「全部のメソッドでエラー通知するしかない」となってきてしまう.

すべてのメソッドでエラー通知

これはこれで合理的ではある. こうしておけば,エラーがどんな順序で起こったかがすべてわかるからだ.

まぁ行数が増えるのでまったくうれしくないのだが…….

しかし問題もある. goはgorutineを使ったスレッド実行がお得意だ. スレッド実行になった場合,スレッド1でCがエラーになって通知されたとしよう.同時にBもAもエラー通知をしてくる. ひとつのエラー発生で3回通知されてしまう.

さらにややこしいことに,スレッド2ではBでエラーが発生したとしよう.同時にAもエラー通知をしてくる.

ふたつのエラーで合計5回のエラー通知がされる.

これは非常にわかりにくい. その上,どのメソッドがどのコンテキスト(今回はどっちのスレッドで実行されていたのか)でエラーになったのかがわかりにくくなる.

これでは通知が来た時,いったいいくつの処理,いくつのスレッドでエラーが発生して,リカバリには何をしたらいいのかがわからなくなってしまう.

複数回通知しても,それらが同じ原因であるなら,同じコンテキストで発生したエラーであることを明示しておかないと,エラー通知の意味がなくなってしまう.

そうなると,じゃぁエラー通知の瞬間にgorutineの情報を得ようと考えるだろう.

しかしそれはできない.

moznion.hatenadiary.com

qiita.com

groups.google.com

できないんだ.

どうにもできない

結局今のところ「エラー通知をうまいことする」というのは非常に難しい問題になっている.

やはり最上位階層で,「これがエラーだったら,別の処理にしよう」という段階で通知を仕込むのが最も適切だろう.

あと,最近思うことなのだが,panic自体はそんなに悪い手法ではないのではないだろうか?

例えばWebサーバを考えた時,panicしなければサーバの処理は継続される. もちろん,そういうレベルのtry-catcheであれば,error型の得意とするところなので,エラー処理を書けばいいだろう.

だが,goを元にした大抵のWebサーバフレームワーク(gojiやmartini)は,panicが起きても,スタックトレースを出して500を返して,次のリクエストを受けられるようになる. つまりサーバのプロセス自体は殺されない.

ということは,むしろエラー通知をしたいレベルのものであれば,error型で拾って,コントローラ層等で通知を考えたり,その後の処理を考えるよりは,panicしてしまって,panicの通知を考えたほうがいいのではないだろうか. スタックトレースが出るのであれば,どこでpanicされたのかは一目瞭然だし,任意のerrorを送ることもできる.

むしろこっちのほうが,通知やリカバリということを考えたら,使い勝手がいい方法なのではないだろうか.