Go 中 error 是值而非异常,禁止用 panic 处理业务错误;应统一用带码、上下文、可嵌套的自定义 error 类型,显式返回、透传、结构化日志并保留堆栈。

Go 中 error 不是异常,别用 panic 处理业务错误
Go 的 error 是值,不是控制流机制。把 fmt.Errorf 或自定义 error 当成“抛异常”来用,会导致调用链断裂、堆栈丢失、日志难追溯。真正该用 panic 的只有程序无法继续的致命状态(如初始化失败、配置严重错乱),而非 HTTP 404、DB 记录不存在这类可预期业务错误。
- HTTP handler 中遇到用户 ID 不存在,应返回
http.StatusNotFound+ 封装后的error,而非panic - 数据库查询返回
sql.ErrNoRows,直接封装为领域错误(如ErrUserNotFound)并向上返回,不要log.Fatal或os.Exit - 所有中间件、handler、service 层函数签名必须显式返回
error,拒绝隐式错误吞没(比如只写if err != nil { return }却不处理或透传)
用自定义 error 类型 + 错误码统一分类,而非字符串匹配
靠 strings.Contains(err.Error(), "timeout") 判断错误类型既脆弱又低效。应该定义带错误码(Code)、可选原始错误(cause)、上下文字段(TraceID、UserID)的结构体,并实现 Unwrap() 和 Error() 方法。
type AppError struct {
Code string
Message string
Cause error
TraceID string
UserID string
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
var (
ErrUserNotFound = &AppError{Code: "USER_NOT_FOUND", Message: "user does not exist"}
ErrInvalidToken = &AppError{Code: "INVALID_TOKEN", Message: "token is expired or malformed"}
)
- 所有错误创建统一走工厂函数(如
NewAppError(code, msg).WithCause(err).WithTraceID(tid)),避免零散&AppError{...} - 下游通过
errors.Is(err, ErrUserNotFound)或errors.As(err, &e)判断类型,不依赖字符串 - 错误码建议用大写蛇形(
DB_CONN_TIMEOUT),和 HTTP 状态码、gRPC 状态码对齐,方便网关/前端映射
在关键路径上用 errors.Join 合并多个子错误,但注意日志可读性
批量操作(如同时调用 3 个下游服务)失败时,单个 error 无法表达多点失败。Go 1.20+ 的 errors.Join 可合并多个 error,但默认输出是换行拼接,日志系统可能切分行导致解析困难。
- 合并后建议用
fmt.Sprintf("batch failed: %v", errors.Join(errs...))包一层,加前缀便于识别 - 若需保留各子错误上下文(如每个子错误带不同
TraceID),应在自定义 error 中支持嵌套[]error字段,而非单纯依赖Join -
errors.Join不改变原有 error 的Unwrap()行为,仍可通过errors.Is检查任一子错误是否匹配目标 error
日志记录 error 时必须保留原始堆栈,避免只打 .Error()
只记录 err.Error() 会丢失发生位置、调用链和 cause 链。Go 官方 fmt 包在 Go 1.19+ 已支持自动打印堆栈(当 error 实现了 Formatter 接口),但前提是你的自定义 error 正确实现了 Format 方法。
立即学习“go语言免费学习笔记(深入)”;
func (e *AppError) Format(f fmt.State, verb rune) {
switch verb {
case 'v':
if f.Flag('+') {
_, _ = fmt.Fprintf(f, "%s\n%+v", e.Error(), e.Cause)
return
}
fallthrough
case 's', 'q':
_, _ = fmt.Fprint(f, e.Error())
}
}
- 日志库(如
zerolog或zap)记录 error 时,传入err本身,而不是err.Error() - 使用
fmt.Printf("%+v", err)或log.Printf("%+v", err)才能触发完整堆栈打印 - 如果用了
github.com/pkg/errors,注意它已被官方errors包部分取代,新项目优先用原生fmt.Errorf("msg: %w", err)+Unwrap
mysql: connection refused 直接返回给前端)。这需要 Code Review 重点卡住,也需要封装好工具函数降低正确做法的使用成本。










