数据验证

go-playground/validator 使用结构体标签声明验证规则,支持字段、跨字段、嵌套结构体、集合和自定义验证。

go get github.com/go-playground/validator/v10

结构体校验

type CreateUserRequest struct {
    Name     string `validate:"required,min=2,max=50"`
    Email    string `validate:"required,email"`
    Password string `validate:"required,min=8"`
    Age      int    `validate:"gte=18,lte=120"`
    Roles    []string `validate:"min=1,dive,oneof=admin editor viewer"`
}

validate := validator.New(validator.WithRequiredStructEnabled())

request := CreateUserRequest{
    Name:  "Alice",
    Email: "alice@example.com",
    Age:   20,
    Roles: []string{"editor"},
}

err := validate.Struct(request)

dive 会进入 Slice、Array 或 Map,继续校验每个元素。验证器实例会缓存结构体信息,应在应用中复用,而不是每次请求都重新创建。

处理错误

验证失败时可以把错误转换为 validator.ValidationErrors,再映射为稳定的 API 错误格式:

var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
    for _, fieldError := range validationErrors {
        fmt.Printf("field=%s rule=%s param=%s\n",
            fieldError.Field(),
            fieldError.Tag(),
            fieldError.Param(),
        )
    }
}

不要把内部结构体名称和未经处理的错误文本直接返回给客户端。可以为字段名和规则建立翻译表,统一输出错误码与用户可读消息。

跨字段与自定义规则

确认密码可以使用内置的跨字段规则:

type RegisterRequest struct {
    Password        string `validate:"required,min=8"`
    ConfirmPassword string `validate:"required,eqfield=Password"`
}

业务特有规则可以注册为自定义标签:

validate.RegisterValidation("username", func(fl validator.FieldLevel) bool {
    value := fl.Field().String()
    return usernamePattern.MatchString(value)
})

type Request struct {
    Username string `validate:"required,username"`
}

自定义验证函数应保持纯粹和快速。需要查询数据库的唯一性等业务规则,应放在 Service 层处理,而不是塞进字段验证器。

实践建议

  • 验证外部输入,但不要重复验证由程序内部构造且已受类型约束的数据。
  • 标签适合字段形状校验;涉及权限、状态流转和数据库查询的规则属于业务逻辑。
  • Gin 默认集成了 validator,但独立创建实例时需要自行注册自定义规则和翻译器。
  • 校验通过不代表输入安全,SQL 参数化、HTML 转义和文件类型检查仍需分别处理。