最近社内でGoのStacktraceを取る方法が話題に上がったので備忘録としてブログにしてみる。
Goでは標準機能としてスタックトレースを取る方法を提供していない。
これは適切なエラーメッセージを書かけばStacktraceは不要という思想なのかもしれないが、やはり大きめのプロジェクトになればStacktraceが欲しくなることはある。
3rdパーティ製のerrorパッケージを使うという手もあるが、Stacktraceを取るだけならそこまで難しいコードにならないため、自作も十分最初の選択肢に入るはず。
ということで、以下に備忘録的にStacktraceを取得できるerrorパッケージのコードを書いておく。
使用感は標準のerrorsパッケージと同じようにしつつ、 Stacktrace
関数にerrorを渡すことでStacktraceであるFrameの配列を取得できるようになっている。
また、 Format
関数でpanicと同じようなフォーマットの文字列を取得できるようにしている。
これにより、panicを解析してくれるようなクラウドサービスへ乗せたときに、エラーレポートも正しく解析してくれる。
唯一気をつけなければならないこととして、パッケージレベル変数でErrorfを使ってしまうとその呼出時点でStacktraceが記録されてしまうため、発生元のStacktraceが正しく取れなくなってしまう。
そのため、パッケージレベル変数でのerror定義にErrorfを使っている場合、警告を出してくれるLinterも用意した。
https://github.com/nametake/nopkgvarerrorf
package errors
import (
"bytes"
"errors"
"fmt"
"runtime"
"strings"
)
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return errors.New(text)
}
// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand. It is
// invalid to include more than one %w verb or to supply it with an operand
// that does not implement the error interface. The %w verb is otherwise
// a synonym for %v.
func Errorf(format string, a ...interface{}) error {
vars := make([]interface{}, 0, len(a))
for _, err := range a {
if e, ok := err.(error); ok {
vars = append(vars, &stackError{
err: e,
frames: frames(1),
})
} else {
vars = append(vars, err)
}
}
for _, v := range vars {
if _, ok := v.(*stackError); ok {
return fmt.Errorf(format, vars...)
}
}
return &stackError{
err: fmt.Errorf(format, vars...),
frames: frames(1),
}
}
// StackTrace returns stack frames from error.
//
// You must use Errorf to add stack frames.
// If not added stack traces, StackTrace returns nil.
func StackTrace(err error) Frames {
stackErr := &stackError{}
for {
if !errors.As(err, &stackErr) {
break
}
err = stackErr.err
}
return stackErr.frames
}
// Frame represents a stack frame.
type Frame struct {
File string
Line int
Function string
}
type Frames []*Frame
func (fs Frames) String() string {
var b strings.Builder
b.WriteString(goroutine())
b.WriteByte('\n')
for _, f := range fs {
b.WriteString(f.String())
b.WriteByte('\n')
}
return b.String()
}
func (e *stackError) Error() string {
return e.err.Error()
}
func goroutine() string {
buf := make([]byte, 64)
buf = buf[:runtime.Stack(buf, false)]
lines := bytes.Split(buf, []byte("\n"))
return string(lines[0])
}
func (e *stackError) Unwrap() error {
return e.err
}
type stackError struct {
err error
frames []*Frame
}
func (f *Frame) String() string {
return fmt.Sprintf("%s(...)\n\t%s:%d", f.Function, f.File, f.Line)
}
// frames gets stack frames with skip.
func frames(skip int) []*Frame {
var frames []*Frame
var pc [64]uintptr
skip += 2
for {
n := runtime.Callers(skip, pc[:])
if n == 0 {
break
}
if frames == nil {
frames = make([]*Frame, 0, n)
}
ff := runtime.CallersFrames(pc[:n])
for {
f, more := ff.Next()
frames = append(frames, &Frame{
File: f.File,
Line: f.Line,
Function: f.Function,
})
if !more {
break
}
}
skip += n
}
return frames
}
func Format(err error) string {
frames := StackTrace(err)
if frames == nil {
return err.Error()
}
var b strings.Builder
b.WriteString(err.Error())
b.WriteByte('\n')
b.WriteByte('\n')
b.WriteString(frames.String())
return b.String()
}