理系学生日記

おまえはいつまで学生気分なのか

pkg/errors はもう外すことができないライブラリ

ここ 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 していくというものです。 WrapWrapf といった関数がそれを実現してくれますが、 非常にシンプルなコードで実現できるのがすごく良いです。

先程のコードは 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 が提供する WrapWrapf の逆操作で、Cause 関数で実現できます。

func Cause(err error) error

もちろん Cause 関数が返却するのは error interface なので、実体を取り出すためには Type Assertion が必要になりますが。