pkg/erorsを使ったGo言語でのエラーハンドリング
tl;dr
- エラーを返すときには
pkg/errors
のerrors.Wrap
でラップして返すとエラーの原因を返せる - エラーを受け取るときには
pkg/errors
のerrors.Cause
で原因を見れる
前置き
以前の記事の
「エラーをチェックするだけでなく、それらを正常に処理しなさい」の項目の話を知ってから、
業務で書いているコードでも以下のようにバシバシfmt.Errorf
を使っていた。
package main import ( "fmt" ) func foo() error { err := bar() return fmt.Errorf("foo error: %v", err) } func bar() error { err := buz() return fmt.Errorf("bar error: %v", err) } func buz() error { return fmt.Errorf("buz error") } func main() { err := foo() if err != nil { fmt.Println(err) return } }
「俺ちゃんとエラー処理してる」感を感じたのも束の間、以下のコードような課題に直面した。
package main import ( "fmt" ) func foo() error { if err := bar(); err != nil { return fmt.Errorf("foo error by bar: %v", err) } if err := fatal(); err != nil { return fmt.Errorf("foo error by fatal: %v", err) } return nil } func bar() error { return fmt.Errorf("bar error") } func fatal() error { return fmt.Errorf("FATAL") } func main() { err := foo() if err != nil { fmt.Println(err) return } }
一見特に問題のあるコードには見えない。しかし、エラーを受ける側が発生したエラーを判断して処理を 変更したい時に非常に困ることになる(実際困った)。
上記のコードの例だと、mainでfoo関数を実行して、そのエラーの内容を出力している。 もしエラーが発生したときは以下のように出力され、どこでどのエラーが発生したのかを知ることはできる。
foo error by bar: bar error
しかし、「foo関数で発生するエラーはそこまで致命的ではないので、翌日の朝にログとして検出できれば良いが、 fatal関数で発生するエラーは致命的で、即アラートを上げたい」というような状況が起こると少し面倒なことになる。
何が問題か
Goのエラーハンドリングの方法として、以下のように自分でエラーを定義してswitch
で処理を切り替えるという方法がある。
package main import ( "errors" "fmt" ) var ( errBar = errors.New("bar error") errFatal = errors.New("fatal error") ) func foo() error { if err := bar(); err != nil { return errBar } if err := fatal(); err != nil { return errFatal } return nil } func bar() error { return errBar } func fatal() error { return errFatal() } func main() { err := foo() switch err { case errBar: fmt.Printf("log: %v\n", err) case errFatal: fmt.Printf("Aleart! : %v\n", err) default: fmt.Println(err) } }
上記のコードの例だと、foo関数はエラー発生時に自作のエラーを返している。 外で定義されているエラーを返しているため、mainはそれをswitchで比較して動作を切り替えることができている。 しかし、この方法だとbar関数やfatal関数で発生したエラーの内容を切り捨てることになる。 もしfoo関数内で複数のエラーが発生する可能性があるとそれらも無視することになってしまう。
「そこでfmt.Errorf
を使えば」と思うが、実はfmt.Errorf
を使うとswitchで比較ができなくなる。
fmt.Errorf
の内部を見てみると、標準パッケージのerrors.New
のラッパーになっている。
そのerrors.New
の中身はerrorString
構造体にメッセージの内容を格納して
返している(ソース)。
つまり、fmt.Errorf
を経由すると、全てのエラーは新たに定義されたerrorString
に変わってしまうことになり、
自分で定義したエラーとは比較ができなってしまう。
どうやって解決したか
エラーの文字列で判別するのかな?、とも一瞬思ったが、調べたらスマートなやり方があった (参考1)。 標準のerrorsパッケージに則ったライブラリのgithub.com/pkg/errorsを 使うと、発生したエラーをハンドリング元まで返すことができるようになる。
package main import ( "fmt" "github.com/pkg/errors" ) var ( errBar = errors.New("bar error") errFatal = errors.New("fatal error") ) func foo() error { if err := bar(); err != nil { return errors.Wrap(err, "handling bar errror") } if err := fatal(); err != nil { return errors.Wrap(err, "handling fatal error") } return nil } func bar() error { return errBar } func fatal() error { return errFatal } func main() { err := foo() switch errors.Cause(err) { case errBar: fmt.Printf("log: %v\n", err) case errFatal: fmt.Printf("Aleart! : %v\n", err) default: fmt.Println(err) } }
foo関数内でエラーが発生した際、errors.Wrap
に発生したエラーとメッセージを渡して返している。
errors.Wrap
を使用して作成したエラーは、errors.Cause
を使用して、一番最初にerrors.Wrap
されたときの
エラーを取り出すことが出来る。
そのため、foo関数で発生したエラーは、bar関数とfatal関数のどちらが原因なのかをmain関数で取り出すことが出来るようになり、
エラーの内容によって処理を切り替えることができるようになる。
pkg/errorsの内部実装を見てみると、errors.Wrap
を使用してエラーを作成する時に、渡されたエラーを保持する構造体を
返すようになっていた。そのため、errors.Wrap
を使用している限りは最初に発生したエラーは消えること無く、
errors.Cause
で取り出せるようになっている。
pkg/errorsの内部は非常にシンプルな実装になっていたので、気になる人は内部の実装をちゃんと見ることをおすすめする。