LoginSignup
1
4

More than 3 years have passed since last update.

【Go】fmt.Errorf("%w %s", err, `実行してる箇所`)でスタックトレースをログに出すという案

Last updated at Posted at 2020-06-17

はじめに

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パッケージを使ったスタックトレース取得の内容の詳細は下記になります。

  1. pc := make([]uintptr, 1) は スタックトレース1つ分を確保
  2. n := runtime.Callers(2, pc) は2スキップ分開始対象にしてpcへバインドします。 0はCallers関数自体、1はWrap関数なので、2にすると0スタックトレースを取りたいWrap(err)を呼んでいる箇所が対象になります。
  3. 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 このアイディアが良いか悪いかフィードバックをいただければと思います。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4