Chapter 06

参数校验与错误处理

深入 validator.v10 的规则与错误翻译,设计业务错误码体系,用统一错误中间件把散落的错误响应收拢到一处。

6.1 核心名词

ValidationErrors
validator 校验失败返回的错误类型,是 FieldError 的切片,可逐字段拿到失败的规则名、字段名、参数。
翻译器(Translator)
universal-translator + validator 翻译组件,把英文错误(如 "failed on 'email' tag")转成中文可读文案。
业务错误码(Error Code)
项目自定义的数字/字符串码,用来区分具体业务失败原因,独立于 HTTP 状态码。
哨兵错误(Sentinel Error)
预定义的 error 变量(如 ErrUserNotFound),用 errors.Is 判等,便于分层传递。
错误包装(Wrapping)
fmt.Errorf("...: %w", err) 保留原始错误链,配合 errors.Is/As 追溯根因。
c.Error()
Gin 把错误挂到 Context 的错误列表(c.Errors),由统一错误中间件在链尾集中处理。
panic / recover
Go 的异常机制,Recovery 中间件用 recover() 兜住未预期 panic,避免整个服务崩溃。

6.2 把校验错误翻译成中文

import (
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    zhTrans "github.com/go-playground/validator/v10/translations/zh"
)

var trans ut.Translator

func initTrans() {
    v, _ := binding.Validator.Engine().(*validator.Validate)
    zhLoc := zh.New()
    uni := ut.New(zhLoc, zhLoc)
    trans, _ = uni.GetTranslator("zh")
    _ = zhTrans.RegisterDefaultTranslations(v, trans)
}

// 把 ValidationErrors 转成 map[字段]中文
func transErr(err error) map[string]string {
    var ve validator.ValidationErrors
    if errors.As(err, &ve) {
        out := map[string]string{}
        for _, fe := range ve {
            out[fe.Field()] = fe.Translate(trans)
        }
        return out
    }
    return map[string]string{"_": err.Error()}
}
字段名也能中文

注册 RegisterTagNameFunc 读取 label tag,可让报错里的字段名变成「邮箱」而非「Email」。例如 Email string `binding:"required,email" label:"邮箱"`

6.3 业务错误码体系

// pkg/errcode/code.go
package errcode

type Error struct {
    Code int
    Msg  string
    HTTP int // 对应的 HTTP 状态码
}

func (e *Error) Error() string { return e.Msg }

var (
    Success       = &Error{0, "success", 200}
    ErrParam      = &Error{10001, "参数错误", 400}
    ErrUnauthorized = &Error{10002, "未登录或登录已过期", 401}
    ErrUserNotFound = &Error{20001, "用户不存在", 404}
    ErrInternal   = &Error{50000, "服务器内部错误", 500}
)

6.4 统一错误处理中间件

让 handler 只管把错误 c.Error(err) 交出去,由中间件在链尾统一渲染:

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next() // 先执行业务,再收错

        if len(c.Errors) == 0 {
            return
        }
        err := c.Errors.Last().Err

        // 业务错误
        var be *errcode.Error
        if errors.As(err, &be) {
            c.JSON(be.HTTP, gin.H{"code": be.Code, "message": be.Msg})
            return
        }
        // 校验错误
        var ve validator.ValidationErrors
        if errors.As(err, &ve) {
            c.JSON(400, gin.H{"code": 10001, "message": "参数错误", "fields": transErr(ve)})
            return
        }
        // 兜底
        c.JSON(500, gin.H{"code": 50000, "message": "服务器内部错误"})
    }
}
// handler 极简:出错就 c.Error + return
func getUser(c *gin.Context) {
    u, err := svc.FindUser(c.Param("id"))
    if err != nil {
        c.Error(errcode.ErrUserNotFound) // 交给中间件
        return
    }
    c.JSON(200, gin.H{"code": 0, "data": u})
}

6.5 panic 恢复与告警

func Recovery(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                logger.Error("panic",
                    zap.Any("error", err),
                    zap.ByteString("stack", debug.Stack()),
                    zap.String("path", c.Request.URL.Path),
                )
                // 这里可接告警:企业微信/飞书/Sentry
                c.AbortWithStatusJSON(500, gin.H{"code": 50000, "message": "服务器内部错误"})
            }
        }()
        c.Next()
    }
}
不要用 panic 做流程控制

Go 的惯例是「错误即值」,业务失败返回 error,而非 panic。panic 只留给「真正不该发生」的情况(空指针、越界、配置缺失)。Recovery 是最后一道保险,不是常规分支。

6.6 校验与错误处理小结