gin 里 error 处理与 c.Errors 机制

结论先行

gin 的 handler 签名没有返回值,错误不能“return 出去”,统一走 c.Errors:用 c.Error(err) 把错误追加到请求的错误列表,由错误处理中间件c.Next() 返回后统一读取、打日志或生成响应。c.AbortWithError(code, err) 则是在追加错误的同时中止 handler 链。

机制要点

概念说明
c.Errors类型 []*Error,保存本请求收集到的错误(实现 error 接口,可 String())
c.Error(err)追加一个错误;不中止链、不写响应
c.AbortWithError(code, err)追加错误 + Abort + 写状态码
Error 类型含 Err、Type(ErrorTypeBind/Private/Public…)、Meta,可分类过滤
c.Errors.ByType(t) / Last()按类型筛选 / 取最后一个错误

推荐实践(集中式错误处理)

  1. 业务 handler / 绑定失败处:c.Error(err)(可加 Meta 带上下文),必要时 Abort。
  2. 注册一个错误中间件放在链的外层:c.Next() 之后遍历 c.Errors,统一记日志、按错误类型映射状态码与响应体。
  3. 对外只暴露安全信息,内部细节进日志——避免把 SQL 错误直接吐给客户端。

代码示例

// 统一错误处理中间件(放最外层)
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next() // 先跑完业务链
        if len(c.Errors) == 0 {
            return
        }
        var first = c.Errors[0]
        status := http.StatusInternalServerError
        if first.Type == gin.ErrorTypeBind {
            status = http.StatusBadRequest
        }
        log.Printf("[err] path=%s err=%v meta=%v", c.Request.URL.Path, first.Err, first.Meta)
        if !c.Writer.Written() { // 业务方还没写响应才接管
            c.AbortWithStatusJSON(status, gin.H{"error": "request failed"})
        }
    }
}

// 业务方用法
if err := svc.Create(c, req); err != nil {
    _ = c.Error(err).SetMeta("create_user") // 收集,让统一中间件兜底
    c.Abort()
    return
}

常见追问 / 记忆点

  • 追问:c.Error 和直接返回 error 的区别?答:gin handler 无返回值,错误必须挂到请求上下文;中间件在链结束后统一可见、可分类、可打日志,天然支持“一处定义响应格式”。
  • 追问:绑定失败的典型错误类型?答:ErrorTypeBind,可单独过滤做 400 提示。
  • 追问:错误处理中间件放哪层?答:尽量靠外(紧跟 Recovery 内侧),才能兜住组内所有路由的错误。
  • 记忆点:收集(c.Error)→ Next 后统一读取(c.Errors)→ 分类映射响应;响应只写一次,别重复。
笔记加载中…