必须使用apierror结构体而非fmt.errorf,因其实现error接口、携带code/message/detail字段,支持errors.as断言和%w包装,确保错误码统一、日志可结构化、前端响应一致、中间件可拦截,而fmt.errorf仅返回字符串,丢失语义与可追溯性。

APIError 结构体必须实现 error 接口,且所有 HTTP handler 中的错误返回必须走该结构体或其衍生类型——否则前端收不到一致的 code 字段,日志里也查不到可定位的业务错误码。
为什么不能直接用 fmt.Errorf 返回业务错误
直接用 fmt.Errorf("user not found") 会丢失错误码、无法区分是参数错还是资源错、前端没法做针对性提示。更关键的是:errors.Is(err, ErrNotFound) 这类判断完全失效,因为没保留原始错误类型或语义。
- HTTP 状态码和业务码混在一起(比如 404 错误可能对应
Code: 1002,但fmt.Errorf不带这个字段) - 日志中只有字符串,无法结构化提取
Code做聚合统计 - 中间件无法识别并透传
Detail字段给运维或监控系统
APIError 必须支持 %w 包装和 errors.As 类型断言
业务层调用下游服务出错时,既要保留原始错误上下文,又要注入业务码。正确写法是:
func GetUserByID(id int) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
// 包装时带上原始 err,同时赋予业务语义
return nil, &APIError{
Code: 404,
Message: "用户不存在",
Detail: fmt.Sprintf("db.FindUser(%d) failed: %v", id, err),
}
}
return user, nil
}
注意:这里不用 fmt.Errorf("%w", err),因为 APIError 本身不是包装器,而是终端错误载体;若需链式传递(如 service → handler → middleware),应在中间层用 fmt.Errorf("service layer: %w", apiErr),但最终响应前必须解包成 *APIError。
错误码常量必须用 iota + 全局 map[int]string 统一管理
硬编码 Code: 404 在多个地方出现,后期改一个漏一个。正确方式是:
const (
CodeOK = iota // 0
CodeInvalidRequest // 1
CodeUnauthorized // 2
CodeUserNotFound // 3
CodeInternalError // 4
)
var CodeMessage = map[int]string{
CodeOK: "success",
CodeInvalidRequest: "请求参数错误",
CodeUnauthorized: "未授权",
CodeUserNotFound: "用户不存在",
CodeInternalError: "服务器内部错误",
}
- 所有 handler 中只用
CodeUserNotFound,不写数字 - JSON 响应中的
Message字段从CodeMessage[err.Code]动态取,方便后续加多语言支持 - Swaggo 文档注释里用
// @Success 200 {object} APIResponse{data=User},错误响应统一标注为@Failure 400 {object} APIError
Gin 中间件里 c.Error() 和 c.Errors 的真实行为
c.Error() 只是把错误塞进 c.Errors 切片,它本身不终止执行;你必须手动 return,否则后续逻辑还会跑。常见翻车点:
- 写了
c.Error(ErrUserNotFound)却没return,导致 panic 或重复响应 - 在中间件里用了
c.AbortWithStatusJSON(400, err),绕过了c.Errors,导致自定义错误拦截失效 -
c.Errors[0].Err是原始 error 值,不是*APIError,必须用errors.As(err, &target)安全转换
真正可靠的拦截写法是:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
var apiErr *APIError
if errors.As(c.Errors.Last().Err, &apiErr) {
c.JSON(apiErr.Code, apiErr)
} else {
c.JSON(500, &APIError{
Code: CodeInternalError,
Message: CodeMessage[CodeInternalError],
Detail: c.Errors.Last().Err.Error(),
})
}
c.Abort() // 防止后续中间件继续写响应
}
}
}
实际项目中最容易被忽略的,是错误码和 HTTP 状态码的映射关系没有文档化——比如 CodeUserNotFound 对应 HTTP 404,但 CodeInvalidRequest 应该返回 400 还是 422?这个决策一旦写死在中间件里,就很难被前端或测试团队感知。建议把映射表单独抽成 YAML 文件,在 CI 阶段校验一致性。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











