fmt.Errorf()でのerrorのwrapはpkg/erorrsのCauseを壊してしまうので注意
どちらか一方に統一されていた場合には困らないが、混在している環境での動作のメモ。
特に、トップレベルでerrors.Cause()
を使っている処理が存在していると危険かもしれないという話。
実験
テキトーに以下のようなコードを書いてみる。直接エラーを返すもの、pkg/errorsでwrapするもの、Errorfでwrapするものについて調べる感じのコード。
package main import ( "fmt" "log" "github.com/pkg/errors" ) var ErrFoo = fmt.Errorf("FOO") func main() { if err := run(); err != nil { log.Fatalf("!! %+v", err) } } func run() error { fmt.Println("foo is Foo?", errors.Is(foo(), ErrFoo)) // true fmt.Println("withWrap-foo is Foo?", errors.Is(withWrap(), ErrFoo)) // true fmt.Println("withWrap-foo cause Foo?", errors.Cause(withWrap()) == ErrFoo) // true fmt.Println("errorf-foo is Foo?", errors.Is(withErrorf(), ErrFoo)) // true fmt.Println("errorf-foo cause Foo?", errors.Cause(withErrorf()) == ErrFoo) // false return nil } func foo() error { return ErrFoo } func withWrap() error { if err := foo(); err != nil { return errors.Wrap(err, "with pkg.errors.Wrap") } return nil } func withErrorf() error { if err := foo(); err != nil { return fmt.Errorf("with errorf: %w", err) } return nil }
実行結果は以下。Causeの方はfalseになる。
foo is Foo? true withWrap-foo is Foo? true withWrap-foo cause Foo? true errorf-foo is Foo? true errorf-foo cause Foo? false
原因
fmt.Errorf()
でwrapされた関数は Cause() error
を実装していないので。
READEMEにも書かれている通りeerrors.Cause()
は以下のようなインターフェイスでwrapされていることを前提にしている。
Using errors.Wrap constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by errors.Cause.
type causer interface { Cause() error }
というわけでmainに近いところで以下のようなコードがあると危険かも。
switch err := errors.Cause(err).(type) { case *MyError: // handle specifically default: // unknown error }
代わりに errors.Is()
や errors.As()
を使うと良い。
ちなみに、asにgo vetを効かせたかったら真面目にlinterを調整するか、errors.As()
を使うように心がけると良い。