直接用 errors.Wrap 会丢失原始堆栈,因为其仅在当前调用点捕获新堆栈,若被包装的错误本身无堆栈(如标准库 errors.New 或第三方简单错误),则原始出错位置信息彻底丢失;需确保底层错误自带堆栈或改用 github.com/cockroachdb/errors 等默认捕获全堆栈的库。

为什么直接用 errors.Wrap 会丢失原始堆栈?
很多 Go 项目一上来就用 github.com/pkg/errors.Wrap 或 fmt.Errorf("xxx: %w", err) 包装第三方错误,结果发现日志里只显示包装后的消息,原始 panic 点或底层调用链没了。根本原因是:如果第三方库返回的错误本身不携带堆栈(比如纯 errors.New 或字符串错误),Wrap 只会在当前行打一个新堆栈,掩盖了真正出问题的位置。
- 典型表现:
redis.Dial失败后,你看到的是connect to redis: failed: dial tcp: lookup redis: no such host,但堆栈指向你调用errors.Wrap的那行,而不是 redis 库内部解析地址失败的真实位置 - Go 1.13+ 的
%w语法只传递错误链,不自动捕获堆栈;要保留原始上下文,得依赖底层错误是否实现了Unwrap()+ 是否自带堆栈 - 第三方模块如
golang.org/x/net/context、database/sql、redis/go-redis多数只返回简单错误,需手动增强
用 github.com/cockroachdb/errors 替代传统包装
它比 pkg/errors 更激进:默认在构造每个错误时捕获完整堆栈,并支持跨 goroutine 透传、延迟格式化、结构化字段注入。对第三方错误做“无损增强”最实用。
- 安装:
go get github.com/cockroachdb/errors - 包装示例:
err := redisClient.Get(ctx, "key").Err() if err != nil { return errors.Wrapf(err, "get key %s from redis", key) } - 关键点:
Wrapf不仅保留原始错误(通过Unwrap),还会在新错误上附加当前堆栈 —— 两者堆栈都可查,用errors.Detail(err)能打印完整调用链 - 兼容性:它实现标准
error接口,所有if errors.Is(err, xxx)、errors.As(err, &t)都能正常工作
如何避免重复包装导致堆栈爆炸?
同一错误被多层函数反复 Wrap,最后日志里堆栈嵌套十几层,真正有用的只有最外层和最内层。必须设规则卡住。
- 只在边界处包装:HTTP handler、RPC 入口、任务队列消费者这类“错误出口”,而不是每个中间层都包一遍
- 检查是否已包装:用
errors.Is(err, yourCustomErrorType)或判断errors.Cause(err) == err(cockroachdb/errors提供errors.IsRoot(err)) - 禁止在 defer 中包装:比如
defer func() { if r := recover(); r != nil { log.Error(errors.Wrap(r, "panic")) } }()—— 这里r是 interface{},不是 error,强行转会导致信息丢失 - 第三方错误统一加前缀而非重写:比如
redis.ErrNil直接用fmt.Errorf("redis: %w", err),不改消息内容,方便下游用errors.Is(err, redis.ErrNil)判断
生产环境必须开启的两个配置
光靠包装不够,得让错误真正有用。两个开关不开,再优雅的包装也白搭。
立即学习“go语言免费学习笔记(深入)”;
- 全局日志输出开启
errors.Detail:logrus/zap 等库集成时,把errors.Detail(err)作为字段注入,而不是只打err.Error() - HTTP 错误响应体包含
errors.SafeText(err)(cockroachdb/errors提供):过滤敏感路径、密码、token,避免把/etc/shadow或数据库连接串直接吐给前端
最容易被忽略的是:第三方模块的错误类型可能带私有字段(如 pgconn.PgError),直接 JSON 序列化会报错,必须用 errors.SafeText 或自定义 MarshalJSON 处理。


















