Carpe Diem

備忘録

fmt.Errorfとxerrors.Errorfと独自エラーと

背景

エラーハンドリングでは

  • エラーが発生した箇所を追うことができる
  • エラーの原因によって処理を分岐することができる

といったことが重要です。

Go 1.13から入ったラップする仕組みにより、エラーメッセージにアノテートしていくことができエラーの発生箇所を追いやすくなりました。
またerrors.Isによりエラー原因による条件分岐もしやすくなりました。

今回はそれらと独自エラーの扱いについて説明します。

環境

  • Go 1.16.7

fmt.Errorfとxerrors.Errorfの違い

まず公式のfmt.Errorfxerrors.Errorfの違いについて説明します。

共通

fmt.Errorfxerrors.Errorfもどちらも: %s, : %v, : %wといったフォーマット文字列を扱うことができます。
: %wの場合はエラーをラップすることができ、返されたエラーはerrors.Unwrapメソッドを実装しているのでerrors.Isなどでエラーのハンドリングが可能です。

func main() {
    err := func1()
    if err != nil {
        fmt.Printf("%s\n", err)
    }
    if errors.Is(err, dbError) {
        fmt.Println("db error")
    }
}

func func1() error {
    err := func2()
    if err != nil {
        return fmt.Errorf("func1 error: %w", err)
    }
    return nil

}

func func2() error {
    err := root()
    if err != nil {
        return fmt.Errorf("func2 error: %w", err)
    }
    return nil

}

var dbError = errors.New("some error")

func root() error {
    return dbError
}

https://play.golang.org/p/YQnHElSA1SP

結果

func1 error: func2 error: some error
db error

違い

xerrors.Errorfは呼び出し元のファイルと行番号を含みます。
つまりfmt.Printf("%+v", err)した時にスタックトレースを出力できます。

func main() {
    err := func1()
    if err != nil {
        fmt.Printf("%+v\n", err)
    }
    if errors.Is(err, dbError) {
        fmt.Println("db error")
    }
}

func func1() error {
    err := func2()
    if err != nil {
        return xerrors.Errorf("func1 error: %w", err)
    }
    return nil

}

func func2() error {
    err := root()
    if err != nil {
        return xerrors.Errorf("func2 error: %w", err)
    }
    return nil

}

var dbError = errors.New("some error")

func root() error {
    return xerrors.Errorf("root error: %w", dbError)
}

https://play.golang.org/p/WpfBy8a05ZJ

結果

func1 error:
    main.func1
        /tmp/sandbox3524972664/prog.go:23
  - func2 error:
    main.func2
        /tmp/sandbox3524972664/prog.go:32
  - root error:
    main.root
        /tmp/sandbox3524972664/prog.go:41
  - some error
db error

注意点

ただしこのスタックトレースのフレーム(ファイルと行番号)が付くのはxerrors.Errorfでラップした場合のみです。
なのでそのままエラーを返した場合はxerrors.Errorfでラップした箇所のみスタックトレースが表示されます。
また間にfmt.Errorfを使った場合はそれより前にxerrors.Errorfを使っていてもフレーム情報が消えてしまいます。

そのままエラー返した場合

func func1() error {
    err := func2()
    if err != nil {
        return err
    }
    return nil

}

func func2() error {
    err := root()
    if err != nil {
        return err
    }
    return nil

}

var dbError = errors.New("some error")

func root() error {
    return xerrors.Errorf("root error: %w", dbError)
}

https://play.golang.org/p/uaaUEkkvC83

結果

root error:
    main.root
        /tmp/sandbox1423853593/prog.go:38
  - some error

間にfmt.Errorfを使った場合

func func1() error {
    err := func2()
    if err != nil {
        return fmt.Errorf("func1 error: %w", err)
    }
    return nil

}

func func2() error {
    err := root()
    if err != nil {
        return err
    }
    return nil

}

var dbError = errors.New("some error")

func root() error {
    return xerrors.Errorf("root error: %w", dbError)
}

https://play.golang.org/p/jbk-x7I-iRT

結果

func1 error: root error: some error

独自エラー

公式のエラーについて整理できたので次は独自エラーの扱い方についてです。

Unwrap実装は必要?

以下のように他のエラーをラップして、errors.Isで条件分岐に使いたい場合は必要です。

func main() {
    err := root()
    if err != nil {
        fmt.Printf("%+v\n", err)
    }
    if errors.Is(err, dbError) {
        fmt.Println("db error")
    }
}

type MyError struct {
    msg string
    err error
}

func (m MyError) Error() string {
    return m.msg
}

func (m MyError) Unwrap() error {
    return m.err
}

func NewError(msg string) error {
    return MyError{
        msg: msg,
        err: nil,
    }
}

func Wrap(err error) error {
    return MyError{
        msg: err.Error(),
        err: err,
    }
}

var dbError = errors.New("some error")

func root() error {
    return Wrap(dbError)
}

https://play.golang.org/p/TBefBdFhbkN

結果

some error
db error

Unwrap()を実装しないと

Unwrap()を実装していないとerrors.Isで判別できません。

func main() {
    err := root()
    if err != nil {
        fmt.Printf("%+v\n", err)
    }
    if errors.Is(err, dbError) {
        fmt.Println("db error")
    }
}

type MyError struct {
    msg string
    err error
}

func (m MyError) Error() string {
    return m.msg
}

func NewError(msg string) error {
    return MyError{
        msg: msg,
        err: nil,
    }
}

func Wrap(err error) error {
    return MyError{
        msg: err.Error(),
        err: err,
    }
}

var dbError = errors.New("some error")

func root() error {
    return Wrap(dbError)
}

https://play.golang.org/p/IvlNqCCzJ_c

結果

some error

スタックトレース情報をつけるには?

fmt.Formatterxerrors.Formatterの2つのinterfaceを実装することで実現できます。
xerrorsのスタックトレースの仕組みに乗っかる形ですね。

fmt.Formatterについては

qiita.com

で分かりやすく説明されてます。

func main() {
    err := func1()
    fmt.Printf("%v\n", err)
    fmt.Println()
    fmt.Printf("%+v\n", err)
}

type MyError struct {
    message string
    frame   xerrors.Frame
}

func (m *MyError) Error() string {
    return m.message
}

// Format implements fmt.Formatter
func (m *MyError) Format(f fmt.State, c rune) {
    xerrors.FormatError(m, f, c)
}

// FormatError implements xerrors.Formatter
func (m *MyError) FormatError(p xerrors.Printer) error {
    p.Print(m.message)
    m.frame.Format(p)
    return nil
}

func New(msg string) error {
    return &MyError{
        message: msg,
        frame:   xerrors.Caller(1),
    }
}

func func1() error {
    err := func2()
    if err != nil {
        return xerrors.Errorf("func1: %w", err)
    }
    return nil
}

func func2() error {
    err := root()
    if err != nil {
        return err
    }
    return nil
}

func root() error {
    return New("oops")
}

https://play.golang.org/p/6NwoAmT0iBe

ポイントは以下です。

結果

エラーの発生箇所と、xerrors.Errorfでラップした箇所のフレーム情報が出力されます。
xerrorsの仕組みに乗っかってるので、先程のxerrors.Errorfでの注意点と同じです。

func1: oops

func1:
    main.func1
        /tmp/sandbox3441195528/prog.go:47
  - oops:
    main.root
        /tmp/sandbox3441195528/prog.go:61

どれを使えばいいか

選択肢としては以下があります。

スタックトレースが要らない場合

fmt.Errorfで十分かと思います。
ただしエラーの発生箇所を追うために各所のエラーハンドリングでラップする必要があります。

スタックトレースが要る場合

a) シンプルな用途

xerrors.Errorfで良いと思います。
ただしフレーム情報を増やすには各所のエラーハンドリングでラップする必要があります。

b) 独自のエラーコードを使いたい

christina04.hatenablog.com

のような独自のエラーコードでハンドリングしたい場合は独自エラーを定義し、

という今回のようなやり方か、

が良いと思います。
前者はフレーム情報を積むためにラップする処理が必要ですが、後者は不要です。

まとめ

fmt.Errorfxerrors.Errorfの違いと、独自エラーを使う場合に必要な実装について説明しました。

参考