fmt.Printf("%+v", err) 无输出是因为标准库 errors.New 和未用 %w 的 fmt.Errorf 不存堆栈帧;仅 github.com/pkg/errors 系列函数及 Go 1.20+ 中显式 %w 包装的 error 支持 %+v 展开堆栈。

为什么 fmt.Printf("%+v", err) 什么都没打出来
因为 errors.New 和没带 %w 的 fmt.Errorf 创建的 error 根本不存堆栈帧,%+v 对它无效——不是格式写错,是它压根没东西可展开。
只有两类 error 支持 %+v 展开堆栈:
-
github.com/pkg/errors系列函数(errors.New、errors.Wrap、errors.WithStack) - Go 1.20+ 中显式用
%w包装的fmt.Errorf("xxx: %w", err)
常见踩坑:
- 混用
errors.New("msg")和errors.Wrap(err, "ctx"):前者无堆栈,包装后只从Wrap调用处开始记,上游位置全丢 -
return errors.New(err.Error()):原始 error 的堆栈被彻底丢弃,新 error 是空堆栈 - 日志里写
log.Print(err)或fmt.Sprintf("%v", err):只触发Error()方法,%+v根本不生效
errors.Wrap 和 errors.WithStack 到底该用哪个
errors.WithStack(err) 只在第一次包装时捕获堆栈,后续再调用不会更新——它复用原始堆栈帧。如果你在多层函数里反复 WithStack,最终看到的仍是第一次的位置。
立即学习“go语言免费学习笔记(深入)”;
errors.Wrap(err, "msg") 每次都记录当前调用点,适合逐层标注上下文,比如:
- DB 层返回
os.ErrNotExist→errors.Wrap(err, "failed to query user") - Service 层处理失败 →
errors.Wrap(err, "failed to build profile")
实操建议:
- 入口处(如 HTTP handler)用
errors.WithStack(err)捕获首次错误位置 - 中间业务层统一用
errors.Wrap(err, "xxx")补充语义,别再套WithStack - 避免在循环或高频路径(如 JSON 解析)里反复
Wrap,runtime.Callers 开销约 300–800ns/次
HTTP handler 里怎么统一捕获 panic 并打印完整堆栈
别等 panic 一路冒到顶层才处理。每个 handler 开头加一个 defer/recover,配合 runtime.Stack 抓当前 goroutine 堆栈:
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
runtime.Stack(buf, false)
log.Printf("PANIC: %s\n%s", r, buf)
}
}()
注意两点:
-
runtime.Stack(buf, false)只打当前 goroutine;若要查所有协程状态,用true,但生产环境慎用(可能卡住) - panic 日志必须用
log.Printf或zap.Error(err)(需配合pkg/errors),不能只写log.Print(r)—— 那只是字符串,没堆栈
同时,对正常返回的 error,也要在 handler 结尾做一次 errors.WithStack 包装(仅非 nil 时),确保日志里能用 %+v 展开。
用 zap 记录 error 堆栈时为什么字段为空
因为 zap.Error(err) 只会自动提取堆栈字段,前提是 err 实现了 stackTracer 接口(比如 pkg/errors 的 error 类型)。标准库 errors.New 返回的 error 不满足这个条件,字段就为空。
验证方法:
- 打印
fmt.Printf("%+v", err)能出堆栈 →zap.Error(err)就能取到 - 打印只有字符串 → zap 字段必然为空
实操建议:
- 所有业务函数返回前,对非 nil error 调用
errors.WithStack或errors.Wrap - 不要在中间层用
fmt.Errorf("xxx: %v", err)—— 这会丢掉所有堆栈信息 - 如果不用第三方库,自己实现
stackTracer接口时,runtime.Caller(2)通常够用,3更准但可能越界返回 nil
最易被忽略的是:堆栈不是越多越好,关键是让第一眼看到真正出问题的那一行——而不是层层包装的工具函数。



















