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() を使うように心がけると良い。

gist