

新闻资讯
技术教程errors.WithStack只在首次调用时捕获堆栈,重复包装不更新;Go 1.13+需自定义stackError类型实现%+v打印堆栈;runtime.Caller比debug.PrintStack更适合结构化日志埋点;HTTP handler应由顶层中间件统一recover并用%+v+debug.Stack()输出完整错误链。
errors.WithStack 包裹错误时,为什么堆栈没显示?因为 errors.WithStack(来自 github.com/pkg/errors)只在首次包装时捕获堆栈,后续调用 errors.WithStack(err) 不会更新——它复用原始堆栈。若你在多层函数中反复包装,最终看到的仍是第一次包装的位置。
errors.WithStack,例如在 handler 或业务入口处errors.Wrap(err, "xxx") 添加上下文,不重复加堆栈github.com/pkg/errors,不是标准库 errors,后者无堆栈能力%+v 打印带堆栈的标准错误?Go 1.13 引入了 fmt.Errorf 的 %w 动词和 errors.Is/errors.As,但原生仍不记录堆栈。要获得类似 pkg/errors 的 %+v 效果,需手动注入:
import (
"errors"
"fmt"
"runtime/debug"
)
func WithStack(err error) error {
if err == nil {
return nil
}
return &stackError{err: err, stack: debug.Stack()}
}
type stackError struct {
err error
stack []byte
}
func (e *stackError) Error() string { return e.err.Error() }
func (e *stackError) Unwrap() error { return e.err }
func (e *stackError) Format(s fmt.State, verb rune) {
if verb == '+' && s.Flag('+') {
fmt.Fprintf(s, "%v\n%s", e.err, e.stack)
return
}
fmt.Fprintf(s, "%v", e.err)
}
之后用 fmt.Printf("%+v", err) 即可打印堆栈。注意:这会显著增加内存分配,生产环境慎用高频路径。
runtime.Caller 和 debug.PrintStack 哪个更适合日志埋点?debug.PrintStack() 直接输出到 stderr,无法控制格式与目标,不适合结构化日志;runtime.Caller 更可控,推荐用于自定义错误构造:
runtime.Caller(1) 获取调用方文件/行号(0 是当前函数)fmt.Sprintf 构造含位置信息的错误消息,例如:fmt.Errorf("failed to parse JSON at %s:%d: %w", file, line, err)
runtime.Caller,有性能开销(约 1–2μs/次)
不要在每层 handler 都 log.Printf,而是把错误统一交给顶层中间件处理:
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
err, ok := rec.(error)
if !ok {
err = fmt.Errorf("panic: %v", rec)
}
log.Printf("PANIC %+v\n%s", err, debug.Stack())
http.Error(w, "Internal Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 使用时:
http.Handle("/api/", ErrorHandler(http.StripPrefix("/api", apiRouter)))
关键点:顶层 recover + %+v + debug.Stack() 组合,才能同时捕获 panic 错误内容和 goroutine 堆栈。中间业务逻辑只需返回标准错误,无需手动打日志。
真正难的是堆栈深度控制——debug.Stack() 默认打印整个 goroutine,而实际只需最近 5 层调用;若需裁剪,得自己解析 debug.Stack() 输出或改用 runtime.Callers 手动采集帧数。