This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/BACKEND/GIN/6-error-handling.md
T

7.9 KiB
Raw Blame History

tags, create time
tags create time
后端
Go
Gin
错误处理
中间件
2026-04-28 00:00

错误处理机制

概述

Gin 提供了一套链式错误收集 + 全局处理的错误管理机制:handler 中调用 c.Error(err) 将错误存入上下文,c.Errors 收集所有错误,全局恢复中间件兜底 panic。理解这套机制可以写出结构化、一致的错误响应。

思考题:如果你的业务函数返回 (result, err),是否需要手动调用 c.Error(err)?还是可以直接写 JSON 响应?(详见下文第 2 节)

正文

1. c.Error → c.Errors 链式错误收集

Gin 允许在请求生命周期内累积多个错误——这在批量操作或多步验证场景中非常有用:

func batchUpdate(c *gin.Context) {
    var items []Item
    c.ShouldBindJSON(&items)

    var errors gin.ErrorList

    for _, item := range items {
        if err := validate(item); err != nil {
            // 将错误写入 Context,不会中断后续处理
            c.Error(&gin.Error{
                Err:  err,
                Meta: item.ID,  // 附加元数据(哪个 item 出错了)
            })
            continue
        }
        if err := saveToDB(item); err != nil {
            c.Error(&gin.Error{Err: err, Meta: item.ID})
        }
    }

    // 取出所有累积的错误
    if len(c.Errors) > 0 {
        c.JSON(http.StatusBadRequest, gin.H{
            "errors": c.Errors.ByType(gin.ErrorTypeBind),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "batch update done"})
}

gin.Error 结构:

type Error struct {
    Err  error             // 原始错误
    Type ErrorType         // 错误类型
    Meta interface{}       // 附加的任意元数据
}

type ErrorType uint8

const (
    ErrorTypeBind ErrorType = 1 << iota // 绑定错误
    ErrorTypeRelease                   // 资源释放错误
    ErrorTypePrivate                   // 内部业务错误
    ErrorTypePublic                    // 暴露给客户端的业务错误
)

c.Errors 常用方法:

方法 作用
c.Errors.ByType(t) 按类型过滤错误
c.Errors.Last() 获取最后一个错误
c.Errors.First() 获取第一个错误
c.Errors.ToStrings() 转为字符串切片

关键理解: c.Error(err) 不终止请求、不回写响应——它只是往 c.errors 切片里追加。你需要显式检查 len(c.Errors) 并返回错误响应。

思考题:c.Error() 和直接 c.JSON(500, ...) 有什么区别?什么时候应该用前者?

2. 业务错误的正确处理方式

大多数情况下,handler 中的错误不需要走 c.Error 链,而是直接返回错误响应:

func getUser(c *gin.Context) {
    id := c.Param("id")

    user, err := userRepository.FindByID(id)
    if err != nil {
        // ✅ 推荐:直接返回统一错误响应
        if errors.Is(err, sql.ErrNoRows) {
            c.JSON(http.StatusNotFound, gin.H{
                "code":    404,
                "message": "user not found",
            })
            return
        }
        // 服务端错误
        c.JSON(http.StatusInternalServerError, gin.H{
            "code":    500,
            "message": "internal server error",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{"data": user})
}

核心原则:

  • 单次错误:直接 c.JSON(code, ...) 返回,不经过 c.Error
  • 批量/多步场景:用 c.Error 收集所有错误,一次性返回
  • panic 异常:交给 gin.Recovery() 兜底

3. 全局恢复中间件 — gin.Recovery()

默认 gin.Default() 已经内置了 Recovery() 中间件——它会捕获所有 panic,生成 500 响应,并打印堆栈到日志:

// gin/recovery.go — 简化版
func Recovery() HandlerFunc {
    return func(c *Context) {
        defer func() {
            if err := recover(); err != nil {
                // 打印堆栈追踪
                debug.PrintStack()
                // 写入错误链
                c.Error(err.(error))
                // 返回 500
                c.AbortWithStatus(JSONErrorCode(http.StatusInternalServerError))
            }
        }()
        c.Next()
    }
}

自定义 Recovery:

r := gin.New()
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
    log.Printf("Panic recovered: %v", recovered)

    // 可以在这里做更多事:发送告警、记录指标等

    if err, ok := recovered.(error); ok {
        c.JSON(500, gin.H{
            "code":    500,
            "message": "internal server error",
            "trace_id": c.GetString("request-id"),
        })
    } else {
        c.JSON(500, gin.H{
            "code":    500,
            "message": "unexpected error",
        })
    }
}))

提问: gin.Recovery() 把 panic 也写进了 c.Errors,这意味着如果你在 Recovery 之后还有中间件,它们能看到这个错误吗?

答案:不能。recover() 后调用了 c.AbortWithStatus(),中间件链已经终止。

4. HTTP 状态码与业务码的映射

生产环境通常维护两层错误码体系:

层级 说明 示例
HTTP 状态码 网络层语义 200 / 400 / 404 / 500
业务错误码 业务语义 40001 参数校验失败 / 40002 余额不足
// 统一错误响应格式
type ErrorResponse struct {
    Code    int    `json:"code"`     // 业务错误码
    Message string `json:"message"`  // 用户可读消息
    Trace   string `json:"trace,omitempty"` // 调试用 trace ID
}

func handleBizError(c *gin.Context, httpCode, bizCode int, msg string) {
    c.JSON(httpCode, ErrorResponse{
        Code:    bizCode,
        Message: msg,
    })
}

// 使用
handleBizError(c, http.StatusBadRequest, 40001, "用户名不能为空")
handleBizError(c, http.StatusNotFound, 40401, "订单不存在")

常见业务码分段约定:

段 HTTP 码 含义
2xxxx 200 成功
40xxx 400 客户端错误(参数、校验)
401xx 401 未认证
403xx 403 无权限
404xx 404 资源不存在
50xxx 500 服务端内部错误

思考题:如果你的 API 同时面向前端和第三方开发者,你觉得业务错误码是放在 JSON body 里好,还是也应该加一个自定义响应头(如 X-Biz-Code)?为什么?

5. 统一错误响应中间件

把错误处理抽成中间件,确保所有路由的响应格式一致:

func errorMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        // 检查是否有积压的错误
        if len(c.Errors) == 0 {
            return
        }

        // 取最后一个(或最严重)的错误
        lastErr := c.Errors.Last()
        switch lastErr.Type {
        case gin.ErrorTypePrivate:
            c.JSON(http.StatusInternalServerError, gin.H{
                "code":    50000,
                "message": "internal server error",
            })
        case gin.ErrorTypePublic:
            if e, ok := lastErr.Err.(*GinError); ok {
                c.JSON(e.HTTPCode, gin.H{
                    "code":    e.Code,
                    "message": e.Message,
                })
            }
        default:
            c.JSON(http.StatusInternalServerError, gin.H{
                "code":    50000,
                "message": "internal server error",
            })
        }
    }
}

注意: 如果 handler 已经通过 c.JSON() 写入了响应体,中间件再写会覆盖或报错。因此需要在 handler 返回前拦截错误,而不是在 c.Next() 之后处理。更常见的做法是封装统一的 c.Success() / c.Error() 辅助方法。

关联笔记