不能直接用fmt.Errorf("xxx")构造错误,因每次调用生成新error实例,==比较恒为false;须统一使用预定义变量(如var ErrNotFound = errors.New("record not found"))或%w包装,禁用%v/字符串拼接,避免敏感信息泄露。

为什么不能直接用 fmt.Errorf("xxx") 构造错误?
因为每次调用 fmt.Errorf 都生成新地址的 error 实例,== 比较永远失败——比如 if err == ErrNotFound 实际上是拿两个不同指针比,结果恒为 false。团队共享模块里若允许随意字符串构造,下游就无法用 errors.Is(err, pkg.ErrNotFound) 做类型判断,错误处理退化成字符串匹配,一重构就崩。
必须统一使用预定义变量或带 %w 的包装:
-
var ErrNotFound = errors.New("record not found")—— 所有模块复用这一个变量 - 包装时只用
fmt.Errorf("failed to load user: %w", err),禁用%v或拼接 - 禁止在错误信息里写
fmt.Errorf("user %d not found", id)—— 敏感 ID 泄露风险高,日志里补上下文更安全
如何让错误在跨模块调用中保持可断言、可分类?
共享模块的 error 必须是结构体类型,实现 Error() 和 Unwrap(),否则 errors.As() 和链式解包失效。例如:
type AppError struct {
Code string
Message string
cause error
}
func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.cause }
这样下游才能:errors.As(err, &target) 提取原始 *os.PathError,或 errors.Is(err, io.EOF) 判断是否超时。
立即学习“go语言免费学习笔记(深入)”;
关键约束点:
- 所有导出错误类型必须带
Code字段,且值来自统一常量池(如user.ErrNotFound.Code == "USER_NOT_FOUND") - 禁止裸 new(
*AppError),必须通过工厂函数创建:user.NewErrNotFound("user 123 not found") - 工厂函数内部校验
Code是否在合法区间,非法则 panic —— 防止错码污染调用链
HTTP handler 层怎么拦截并标准化共享模块抛出的错误?
共享模块只负责构造和传递错误,不负责序列化。HTTP 层必须设中间件统一拦截 error 类型,否则每个 handler 都要重复写 json.Marshal 和状态码映射。
典型拦截逻辑:
- 若错误实现了
ErrorCoder接口(含Code()、HTTPStatus()),则转成{"code": "...", "message": "...", "trace_id": "..."} + 404 - 若错误是
io.EOF或context.DeadlineExceeded,映射为"TIMEOUT"码并返回 408 - 其他未识别错误一律转为
"SYSTEM_ERROR",状态码 500,且不暴露err.Error()给前端
注意:中间件必须检查 err != nil 后立即 return,不能只 log 就继续执行——共享模块的错误意味着业务流程已中断,后续代码拿到空数据却无感知。
并发场景下如何避免错误被 goroutine 吞掉?
共享模块若提供并发方法(如 DoInParallel()),必须显式支持错误聚合,不能依赖调用方自己 channel 收集。
推荐方式是返回 error 而非 chan error:
- 用
errgroup.Group管理子任务,eg.Wait()返回第一个非 nil 错误 - 若需全部错误,返回
[]error并约定:空 slice 表示成功,非空表示部分失败(不是全失败) - 禁止在 goroutine 内部只
log.Printf("%v", err)—— 这等于静默丢弃,上游无法决策重试或降级
最易被忽略的是:共享模块的并发方法若用了 context.WithCancel,必须确保任一子任务出错时 cancel 全局 ctx,并等待其余 goroutine 退出后再返回错误——否则可能漏掉正在运行的错误。


















