Goでスタックトレースを上書きせずにエラーをラップする方法

こんにちは。eKYC開発チームの藤本です。

eKYCのサーバーサイドではpkg/errorsパッケージを使用してGoのスタックトレースを記録しています。スタックトレースは標準のerrorsパッケージではサポートされていませんが、エラー発生時のスタックトレースがわかるとエラーの解決が楽になるので、是非記録しておきたい情報です。

pkg/errorsパッケージを使用してスタックトレースを記録するには、エラーを初期化するときにerrors.New, errors.Errorf関数を使用するか、既存のエラーに対してerrors.WithStack関数を使用します。こうすると、作成されたエラーerrに対してerr.StackTraceメソッドを呼び出すことによりスタックトレースを取得することができ、err.Formatメソッドの出力にもスタックトレースが含まれるようになります。

こうして作成したエラーをラップするとどういう挙動になるでしょうか?

errors.Wrap関数を使用した場合

pkg/errorsには、errors.Wrapというその名の通りにエラーをラップしてくれる関数があるので、これを使用して作成したエラーがどういう挙動をするのかを実際にコードを書いて確認してみます。

コード

package main

import (
    errs "errors"
    "fmt"
    "github.com/pkg/errors"
)

func main() {
    err := errors.New("error")

    we := errors.Wrap(err, "wrap")

    fmt.Println("### Error() ###")
    fmt.Printf("%v\n\n", we.Error())
 
    fmt.Println("### StackTrace() ###")
    if e, ok := we.(interface{ StackTrace() errors.StackTrace }); ok {
        fmt.Printf("%+v\n\n", e.StackTrace())
    }

    fmt.Println("### Formatter ###")
    if e, ok := we.(fmt.Formatter); ok {
        fmt.Printf("%+v\n\n", e)
    }

    fmt.Println("### Unwrap() ###")
    fmt.Printf("%v\n", errs.Is(we, err))
}

出力

### Error() ###
wrap: error

### StackTrace() ###

main.main
    /tmp/sandbox309561315/prog.go:12
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371

### Formatter ###
error
main.main
    /tmp/sandbox309561315/prog.go:10
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371
wrap
main.main
    /tmp/sandbox309561315/prog.go:12
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371

### Unwrap() ###
true

Go Playground

StackTraceメソッドの出力は、errros.Wrap関数を呼び出した時点(12行目)のスタックトレースになってしまい、元々のエラーの発生箇所(10行目)がわからなくなってしまいます。Formatメソッドの出力には、ラップした時点のスタックトレースと元々のスタックトレースが両方出力されるようになります。
標準パッケージのIs関数で元々のエラーとの一致判定を行うことで、作成したエラーがUnwrapメソッドをサポートしていてラップされた状態になっていることも確認しています。(Is関数の仕様)

エラーの原因を特定するためには、元々のエラーの発生箇所を知りたいので、それがわからなくなってしまったり、余分な情報がついて読みづらくなるのはできることなら避けたいところです。

errors.WithMessage関数を使用した場合

命名からは一見わかりませんが、errors.WithMessage関数もエラーをラップしてくれるので、これを使用して作成したエラーの挙動も同様のコードを書いて確認してみます。

コード

package main

import (
    errs "errors"
    "fmt"
    "github.com/pkg/errors"
)

func main() {
    err := errors.New("error")

    me := errors.WithMessage(err, "message")

    fmt.Println("### Error() ###")
    fmt.Printf("%v\n\n", me.Error()) 

    fmt.Println("### StackTrace() ###")
    if e, ok := me.(interface{ StackTrace() errors.StackTrace }); ok {
        fmt.Printf("%+v\n\n", e.StackTrace())
    }

    fmt.Println("### Formatter ###")
    if e, ok := me.(fmt.Formatter); ok {
        fmt.Printf("%+v\n\n", e)
    }

    fmt.Println("### Unwrap() ###")
    fmt.Printf("%v\n", errs.Is(me, err))
}

出力

### Error() ###
message: error

### StackTrace() ###
### Formatter ###
error
main.main
    /tmp/sandbox076104167/prog.go:10
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1371
message

### Unwrap() ###
true

Go Playground

こちらの出力では、スタックトレースをが上書きされずにラップできていることがわかります。ただし、StackTraceメソッドは使えないため、Formatメソッドの出力結果でスタックトレースを確認する必要があります。

まとめ

エラーをerrors.Wrap関数とerrors.WithMessage関数でラップした場合の挙動を比較してみました。StackTraceメソッドが使えないというデメリットはありますが、errors.WithMessage関数を使うとスタックトレースを上書きせずに簡単にエラーをラップできるので是非使用してみてください。