errors.New不记录堆栈因其仅存储字符串,pkg/errors则通过runtime.Caller显式捕获调用位置;推荐用Wrap/WithStack替代,注意Cause与Unwrap语义差异及Caller偏移选择。

为什么 errors.New 打不出堆栈,而 pkg/errors 可以
因为 errors.New 只存字符串,不捕获调用位置;pkg/errors 在构造错误时主动调用 runtime.Caller 记录文件、行号、函数名。这不是魔法,是显式快照。
实操建议:
- 别用
errors.New或fmt.Errorf(无%w)封装底层错误——会丢堆栈 - 用
errors.Wrap或errors.WithStack替代,它们在包装时才记录当前帧 -
errors.WithStack(err)适合“首次出错就加堆栈”,errors.Wrap(err, "failed to parse")适合逐层标注上下文
用 errors.Cause 和 errors.Unwrap 区分“根本原因”和“包装痕迹”
Go 1.13+ 的 errors.Unwrap 只解一层,而 pkg/errors.Cause 会一直剥到最内层非包装错误(比如原始 os.PathError)。两者语义不同,混用容易误判。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 用
errors.Is(err, os.ErrNotExist)失败 → 实际应先errors.Cause(err)再比对 - 日志里打印
err.Error()看似有堆栈,但 JSON 序列化后只剩字符串 → 堆栈信息已丢失 - HTTP handler 中直接
return err给客户端 → 暴露内部路径和行号,有安全风险
runtime.Caller 的偏移值怎么选:2 还是 3?
写自定义错误包装函数时,runtime.Caller(2) 通常指向调用你包装函数的地方;runtime.Caller(3) 才是真正出错的业务代码行。选错会导致堆栈顶显示你的工具函数,而不是用户代码。
参数差异:
-
runtime.Caller(0)→Caller函数自己 -
runtime.Caller(1)→ 你的包装函数入口 -
runtime.Caller(2)→ 调用你包装函数的那一行(常够用) -
runtime.Caller(3)→ 再往上一层,比如从http.HandlerFunc里调用你函数的位置(更准,但可能越界返回 nil)
建议默认用 2,调试时发现顶层不对再试 3。
生产环境要不要保留完整堆栈?性能和可读性怎么平衡
每次 Wrap 都触发 runtime.Callers,采集几十帧耗时约 300–800ns,在高并发错误路径上不可忽视。而且堆栈太长反而掩盖关键上下文。
实操建议:
- 开发/测试环境:用
errors.Wrap全链路覆盖 - 生产环境:只在关键入口(如 HTTP handler、CLI command)做一次
errors.WithStack,内部逻辑用fmt.Errorf("%w", err)传递 - 避免在循环或高频路径(如 codec 解析)里反复
Wrap,会放大开销 - 日志输出前用
fmt.Sprintf("%+v", err)才能展开堆栈;%v或Error()只显示第一行
堆栈不是越多越好,关键是让第一眼看到的那几帧,确实是人该关心的地方。











