fmt.Errorf()とerrorsのセットとgolang.org/x/xerrorsは異なる

前回の記事でfmt.Errorf()がpkg/errors.Wrap()を殺すという話を書いた。これはもう少し雑に言えば、エラー時に位置情報を記録したいということだった。

そしてこれについて以下の様な発言をしつつ調べていたところ、かつてのpolyfilだと思っていたgolang.org/x/xerrorsが、go1.13以降であっても標準ライブラリのerrorsとfmtパッケージのセットとの間に違いがあることに気づいた。

個人的に気にしていた違いは以下。

  • fmt.Errorf()とerrorsのセット は位置情報を記録しない
  • golang.org/x/xerros は位置情報を記録する

両者は同じものだと思っていたが違っていた。

fmt.Errorf()とerrorsのセット

位置情報を記録しない。

package main

import (
    "fmt"
    "io"

    "github.com/k0kubun/pp"
)

func main() {
    err := fmt.Errorf("xxx: %w", fmt.Errorf("yyy: %w", io.EOF))
    fmt.Printf("!! %+v", err)
    pp.Println(err)

    // &fmt.wrapError{
    //   msg: "xxx yyy EOF",
    //   err: &fmt.wrapError{
    //     msg: "yyy EOF",
    //     err: &errors.errorString{
    //       s: "EOF",
    //     },
    //   },
    // }
}

golang.org/x/xerrors

位置情報を記録する。

package main

import (
    "fmt"
    "io"

    "github.com/k0kubun/pp"
    "golang.org/x/xerrors"
)

func main() {
    err := xerrors.Errorf("xxx: %w", xerrors.Errorf("yyy: %w", io.EOF))
    fmt.Printf("!! %+v\n", err)
    pp.Println(err)
    // &xerrors.wrapError{
    //   msg: "xxx",
    //   err: &xerrors.wrapError{
    //     msg: "yyy",
    //     err: &errors.errorString{
    //       s: "EOF",
    //     },
    //     frame: xerrors.Frame{
    //       frames: [3]uintptr{
    //         0x10cc030,
    //         0x10cde4d,
    //         0x1031e0a,
    //       },
    //     },
    //   },
    //   frame: xerrors.Frame{
    //     frames: [3]uintptr{
    //       0x10cc030,
    //       0x10cdea7,
    //       0x1031e0a,
    //     },
    //   },
    // }
}

%+vの表示

実際のところ前回の記事と同様のコードを書いてみるといい感じの出力を返してくれる。main()の中でf(),g(),h()と順に読んでいくようなコード。

スタックトレースがほしい場合には、xerrorsを使うようにしてみるのもありかもしれない。

2020/09/19 08:38:24 ! on f: on g: hmm
2020/09/19 08:38:24 !!on f:
    main.f
        ./03errors/main03.go:15
  - on g:
    main.g
        ./03errors/main03.go:18
  - hmm

このときのコードは以下。

package main

import (
    "fmt"
    "log"

    "golang.org/x/xerrors"
)

func main() {
    log.Printf("! %v", f())
    log.Printf("!!%+v", f())
}
func f() error {
    return xerrors.Errorf("on f: %w", g())
}
func g() error {
    return xerrors.Errorf("on g: %w", h())

}
func h() error {
    return fmt.Errorf("hmm")
}

という情報が試してみるまで見つけられなかったのがとても不思議。

もう少し詳しく

実際のところドキュメントを真面目に読むとGo 2のproposalの実装と書かれていた。

Package xerrors implements functions to manipulate errors. This package is based on the Go 2 proposal for error values:

https://golang.org/design/29934-error-values

These functions were incorporated into the standard library's errors package in Go 1.13: - Is - As - Unwrap Also, Errorf's %w verb was incorporated into fmt.Errorf. Use this package to get equivalent behavior in all supported Go versions. No other features of this package were included in Go 1.13, and at present there are no plans to include any of them.

実は位置情報の記録はこの内の「No other features」に含まれていたということのよう。

gist

参考

misc

zennを始めてみた。続くかもしれないし、続かないかもしれない。

zenn.dev

fmt.Errorf(" : %w", ...)は、pkg/errors.Wrap()を殺す

go1.13以降、github.com/pkg/errorsから、標準ライブラリのerrorsとfmtを使う形への乗り換えが行われていたり行われていなかったりする1

乗り換えの方法についてはerrors.Is()errors.As()の使い分けに終止している事が多い気がする。それ以外の変更点で気になったことについてメモをしておく。

pkg/errors.Wrap()

その前におさらい。

pkg/errors.Wrap() は2つの機能を持っている。

前者は fmt.Errorf("%w:", ...) で対応できるようになった。

一応pkg/errorsの実行例も表示しておく。実行例は以下の通り。

%+vでSprintfなどした場合に、ちょっとした修飾付きでスタックトレースを含んだ表現を出力してくれる。これは一般的な言語のpanic時のトレースに近い表現。

2020/09/19 08:07:00 ! on f: on g: hmm
2020/09/19 08:07:00 !!hmm
on g
main.g
    ./00errors/main00.go:18
main.f
    ./00errors/main00.go:15
main.main
    ./00errors/main00.go:12
runtime.main
    /opt/local/lib/go/src/runtime/proc.go:204
runtime.goexit
    /opt/local/lib/go/src/runtime/asm_amd64.s:1374
on f

この出力を得たのは以下の様なコード。f,g,hという順に呼ばれている。

package main

import (
    "fmt"
    "log"

    "github.com/pkg/errors"
)

func main() {
    log.Printf("! %v", f())
    log.Printf("!!%+v", f())
}
func f() error {
    // 本来errors.Wrap()を使うのは最下部のものだけで良い
    // しかし、いろいろな関係上全部errors.Wrap()でwrapしてしまうコードもみかける
    return errors.WithMessage(g(), "on f")
}
func g() error {
    return errors.Wrap(h(), "on g")
}
func h() error {
    return fmt.Errorf("hmm")
}

fmt.Errorf("%w", ...)

fmt.Errorf("%w", ...) で同様の例を書いてみる。"%+v"の場合も"%v"の場合も同様の結果になる。

2020/09/19 08:07:18 !  on f: on g: hmm
2020/09/19 08:07:18 !! on f: on g: hmm

このときのコードは以下。

package main

import (
    "fmt"
    "log"
)

func main() {
    log.Printf("!  %v", f())
    log.Printf("!! %+v", f())
}
func f() error {
    return fmt.Errorf("on f: %w", g())
}
func g() error {
    return fmt.Errorf("on g: %w", h())
}
func h() error {
    return fmt.Errorf("hmm")
}

fmt.Errorf("%w", ...) だけを使った場合にスタックトレースが得られないということは理解している。ここまでが前提の確認

上段のfmt.Errorf("%w", ...)

2つを組み合わせたときはどうか。実は上段のfmt.Errorf("%w", ...)がerrors.Wrap()を無効にする。可能なら"%+v"で取り出したときには下層のerrors.Wrap()を見て欲しいがそんなことは起きない。

2020/09/19 08:13:53 ! on f: on g: hmm
2020/09/19 08:13:53 !!on f: on g: hmm

以下の様に書き換えてみる。このようにした場合にfのfmt.Errorf("%+v", ..)がgのerrors.Wrap()を覆い隠してしまう。

package main

import (
    "fmt"
    "log"

    "github.com/pkg/errors"
)

func main() {
    log.Printf("! %v", f())
    log.Printf("!!%+v", f())
}
func f() error {
    return fmt.Errorf("on f: %w", g())
}
func g() error {
    return errors.Wrap(h(), "on g")
}
func h() error {
    return fmt.Errorf("hmm")
}

fmt.Errorf("%w",...)を利用したときに内部で作られるfmt.wrapErrorは、実行時にエラーメッセージの文字列を組み立ててしまい、Format()においてはそれをそのまま返すというような実装になっている。この例では"on f: on g: hmm"という文字列が実行時に作られ、"%+v"を介してもそれがそのまま返されることになる。内部の値を覆い隠してしまうことになる。

&fmt.wrapError{
  msg: "on f: on g: hmm",
  err: &errors.withStack{
    error: &errors.withMessage{
      cause: &errors.errorString{
        s: "hmm",
      },
      msg: "on g",
    },
    stack: &errors.stack{
      0x10d7258,
      0x10d7146,
      0x10d70c5,
      0x1035529,
      0x1063cc1,
    },
  },
}

スタックトレースを望んだコードにおいて、errors.Wrap()と混在させたいと思うのなら、この点は注意が必要かもしれない。

なぜスタックトレースが欲しくなるか?

議論がありそうな気がするが、どちらかといえば個人的には常にスタックトレースが欲しくなる派。

悲観論者なので、すべてを自分の手で書きこだわり抜いたコードだけを見て生きていけるとは思っていない。誰が書いたかもわからないようなコードをメンテすることのほうが多いと思っている節さえある。その上その様なコードの方がエラーに遭遇する率は高く、困った事態になることが多い気がしている。そのときに何としてでも原因に気づけるような糸口がほしくなる。grepなりをするための情報がどの様な形であっても良いから残っていてほしい。

gist

参考

追記

golang.org/x/xerrorsに関する記事を書いた https://pod.hatenablog.com/entry/2020/09/19/084210


  1. errors.Unwrap()を使ったあれこれとfmt.Errorf()を使うこと。まぁ新規の場合は基本的にはこちらを使っているような気がする