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()を使うこと。まぁ新規の場合は基本的にはこちらを使っているような気がする