ここ 2 ヶ月くらいずっと golang でバックエンドを書いています。
その中で、絶対に外せないなと思っているものの1つが pkg/errors
です。
ぼくがこのライブラリを知ったのは、以下の記事でした。
当時は Golang とは無縁の生活を送っていたので、まさか自分が使うことになるとは思いませんでしたが…。
問題
Golang におけるエラーハンドリングの問題
Golang においては、以下のようなイディオムが多数登場してきます。
if err != nil { return err }
処理でエラーが発生しているかをチェックし、エラーが発見されていることが検知されたら (err != nil
) 上位層にそのエラーを返却する。
問題になるのは、ここで返却されているのは元々のエラーの情報のみであって、その「エラーが発生したコンテキスト」の情報が上位層に返却できていないことです。
結果として、最上位層の処理 (例えば API のレイヤ) で log.Errorf(err)
と記述しても、ログファイルには No such file or directory
としか出力されません。
いったい何の処理で、どのファイルがなかったからエラーになったんだろう」と頭を悩ませることになってしまいます。
コンテキスト情報
標準ライブラリにおいては、コンテキスト情報を付与するために、例えば以下のようなコードを使うことができます。
if err != nil { return fmt.Errorf("failed to load app.yaml: %v", err) }
これはこれで意味があることですが、ここでの問題は元々のエラーである err
が文字列の情報 (%v
) のみを残して消え去ってしまいます。
たとえば io.EOF
のチェックコードが、上記のようなコンテキスト付与によって動かなくなるのは問題です。
Dave Cheney のエントリでは、エラー内容を error の実装クラスで表現するパターンを Sentinel Errors と名付け「避けるべき」と言っています。 しかし、ご存知の通りこのパターンは標準ライブラリのいくつかでも採用されているため、コンテキスト情報の付与のすべてを上記のような形で解決することはできません。
pkg/errors
が解決するもの
pkg/errors は、まずこのコンテキスト付けの方法を解決します。
その方法は Java にも良く似ていて、オリジナルの error
を cause として、どんどん error を Wrap Up していくというものです。
Wrap や Wrapf といった関数がそれを実現してくれますが、
非常にシンプルなコードで実現できるのがすごく良いです。
先程のコードは pkg/errors を使うと以下のような形で、エラーが発生したコンテキストを付与することが可能です。
if err != nil { return errors.Wrap(err, "failed to load app.yaml") }
コンテキストを付与するとともに、オリジナルのエラー (err
) は cause として内部に保持した新しい error
を作成し、それを上位層に返却することができます。
上位層では、書式付きの出力で %s
を指定すれば、そのコンテキスト情報が再帰的に出力されます。
例えば上記のコードを走らせると、
package main import ( "fmt" "github.com/pkg/errors" ) func main() { err := errors.Wrap(func() error { return errors.Wrap(func() error { return errors.New("original error") }(), "context in inner method") }(), "context in main") fmt.Printf("error occured: %s\n", err) }
その結果は以下のようになって、どういう流れでエラーが発生したのかを確認することができます。
error occured: context in main: context in inner method: original error
errors
としては内部でスタックトレースを保持していて、上記のコードで %s
を %+v
に変更するだけで、以下のようなスタックトレースを出力することが可能です。
(pkg/errors の提供する関数を使うと、このようなスタックトレースそのものを取り出すことも可能です)
今のプロジェクトでは、ログファイル出力時はスタックトレースを出力するようにして、どういう流れでエラーが伝搬していったのかを確認できるようにしています。
error occured: original error main.main.func1.1 /Users/kiririmode/main.go:12 main.main.func1 /Users/kiririmode/main.go:13 main.main /Users/kiririmode/main.go:14 runtime.main /usr/local/Cellar/go/1.11.1/libexec/src/runtime/proc.go:201 runtime.goexit /usr/local/Cellar/go/1.11.1/libexec/src/runtime/asm_amd64.s:1333 context in inner method main.main.func1 /Users/kiririmode/main.go:11 (snip)
エラーを取り出す
Sentinal errors のパターンにおいては、original のエラーを取り出すことが必要になります。
これは pkg/errors が提供する Wrap
や Wrapf
の逆操作で、Cause
関数で実現できます。
func Cause(err error) error
もちろん Cause
関数が返却するのは error
interface なので、実体を取り出すためには Type Assertion が必要になりますが。