应定义带字段和方法的自定义错误类型替代 fmt.Errorf 直接拼接,便于分类捕获、日志打点和国际化;合理使用 %w 透传关键底层错误,避免滥用导致错误链过长;结合 context.Context 提前检查超时/取消信号,统一错误处理策略。

用自定义错误类型替代 errors.New 和 fmt.Errorf
直接拼接字符串的错误(如 fmt.Errorf("failed to read file %s: %w", path, err))在后期难以分类捕获、日志打点或国际化。应定义带字段和方法的错误类型,比如:
type FileReadError struct {
Path string
Code int
Err error
}
func (e *FileReadError) Error() string {
return fmt.Sprintf("read file %s failed (code %d): %v", e.Path, e.Code, e.Err)
}
func (e *FileReadError) Is(target error) bool {
_, ok := target.(*FileReadError)
return ok
}
这样能用 errors.Is 精准判断错误类型,也方便加 Unwrap() 支持链式错误。
- 避免所有错误都用
fmt.Errorf包裹,尤其不要只为了加前缀而多套一层 - 若错误无需携带额外状态,优先用
errors.Join组合多个错误,而非嵌套%w - 导出的错误类型建议加
IsXxx()辅助方法(如IsPermissionDenied()),降低调用方的类型断言成本
统一错误包装策略:何时用 %w,何时不用
%w 是为保留原始错误上下文而设计的,但滥用会导致错误链过长、日志冗余、调试困难。关键判断依据是「下游是否需要重试、降级或特殊处理该底层错误」。
- 数据库操作失败时,若上层要根据
pgconn.PgError的SQLState做重试,必须用%w透传 - HTTP handler 中调用业务逻辑后,只需返回用户友好的提示(如 “请求参数无效”),就不该用
%w暴露内部json.UnmarshalError - 日志记录前用
errors.Unwrap或errors.Is判断是否需脱敏,避免敏感路径/SQL 被打到日志里
错误处理与 context.Context 协同使用
Go 的 context.Context 本身不是错误机制,但它决定了错误是否“已过期”。很多错误(如 context.DeadlineExceeded、context.Canceled)本质是控制流信号,不该被当成业务异常处理。
立即学习“go语言免费学习笔记(深入)”;
- 函数签名中同时接收
ctx context.Context和返回error时,先检查ctx.Err() != nil,再决定是否执行后续逻辑 - 不要把
ctx.Err()用%w包进新错误——它已是标准错误,重复包装反而干扰errors.Is(err, context.DeadlineExceeded)判断 - 在中间件或 defer 中统一检查
ctx.Err()并提前返回,避免无谓的资源申请和计算
避免错误处理逻辑分散在业务代码中
常见反模式是每个 if err != nil 都写一堆日志、回滚、返回不同 HTTP 状态码。这会让核心逻辑被淹没,且难以统一变更策略。
- 提取共用的错误转译函数,例如
httpError(err)根据错误类型返回对应 status code 和 message - 用中间件(如 Gin 的
recovery或自定义errorHandler)统一捕获 panic 和未处理错误,而不是在每个 handler 里手写defer - 对数据库事务,用
defer func() { if r := recover(); r != nil || err != nil { tx.Rollback() } }()封装,而非每处都手动 Rollback
%w 包装,往往说明职责边界模糊了——该停下来重新划分错误归属和处理层级。










