はじめに
Go言語にて
Before (Issue)
if err != nil {
return err
}
だけしてる場合は出力結果が例えば
cause of error in repo
このようにエラーメッセージだけ出力され、エラー箇所が分かりづらい問題があるあるだと思います。
After (One of solutions)
後述のWrap関数を使い、スタックトレースを取りたい各エラー箇所をreturn Wrap(err)
にするだけで、下記のようなスタックトレースを付けて出力することができます。
cause of error in repo /tmp/sandbox244131559/prog.go:27 main.repo
/tmp/sandbox244131559/prog.go:39 main.service
/tmp/sandbox244131559/prog.go:49 main.handler
環境
- go version go1.14.2 darwin/amd64
実装
以下のコードで説明します。
下記のコードは こちらの Go Playground 上 で試せます。
package main
import (
"errors"
"fmt"
"runtime"
)
func Wrap(err error) error {
if err == nil {
return err
}
// スタックトレースの取得
pc := make([]uintptr, 1)
n := runtime.Callers(2, pc)
frames := runtime.CallersFrames(pc[:n])
frame, _ := frames.Next()
s := fmt.Sprintf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
// スタックトレースを含めてWrapして返す
return fmt.Errorf("%w %s", err, s)
}
func repo() error {
err := errors.New("cause of error in repo")
return Wrap(err)
}
type serviceError struct {
error
code int
}
func service() error {
err := repo()
if err != nil {
return &serviceError{
error: Wrap(err),
code: 10,
}
}
return nil
}
func handler() error {
err := service()
if err != nil {
return Wrap(err)
}
return nil
}
func main() {
err := handler()
if err != nil {
fmt.Println(err)
/*
cause of error in repo /tmp/sandbox818611801/prog.go:27 main.repo
/tmp/sandbox818611801/prog.go:39 main.service
/tmp/sandbox818611801/prog.go:49 main.handler
*/
}
var e *serviceError
if errors.As(err, &e) { // Unwrap
fmt.Println(e)
/*
cause of error in repo /tmp/sandbox334532468/prog.go:27 main.repo
/tmp/sandbox334532468/prog.go:39 main.service
*/
}
}
解説
Wrap(err error) error
関数は
- オリジナルのエラーを受け取り、
-
実行してる箇所
をruntimeパッケージを使って取得し、 - fmt.Errorfの
%w
オプションでスタックトレースを付けてオリジナルのエラーをラップして返します。
runtimeパッケージを使ったスタックトレース取得の内容の詳細は下記になります。
-
pc := make([]uintptr, 1)
は スタックトレース1つ分を確保 -
n := runtime.Callers(2, pc)
は2スキップ分開始対象にしてpcへバインドします。 0はCallers関数自体、1はWrap関数なので、2にすると0スタックトレースを取りたいWrap(err)
を呼んでいる箇所が対象になります。 -
frames := runtime.CallersFrames(pc[:n])
によりruntime.Frame
型にして取得します。Go1.7以降このFrameを利用するのが推奨とのことです。
より詳細は公式パッケージを参照ください。 https://golang.org/pkg/runtime/#Callers
また、上記の例でのserviceError
型などのカスタムエラー型をWrapしても、もちろんerrors.As
, errors.Is
メソッドでUnwrapすることができます。
おわりに
Wrap関数をパッケージ化しました。https://github.com/momotaro98/stew このアイディアが良いか悪いかフィードバックをいただければと思います。