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,但默认输出是换行拼接,日志系统可能切分行导致解析困难。