自定义验证 #

一、自定义验证概述 #

1.1 为什么需要自定义验证 #

内置验证规则无法满足所有业务需求:

go
type User struct {
    Phone    string `binding:"required,???"` // 需要验证手机号格式
    IDCard   string `binding:"required,???"` // 需要验证身份证号
    Username string `binding:"required,???"` // 需要验证用户名规则
}

1.2 自定义验证流程 #

text
定义验证函数 → 注册验证器 → 使用验证标签

二、注册自定义验证器 #

2.1 基本注册 #

go
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

type User struct {
    Phone string `binding:"required,phone"` // 使用自定义验证器
}

func main() {
    r := gin.Default()
    
    // 获取验证器
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        // 注册自定义验证器
        v.RegisterValidation("phone", validatePhone)
    }
    
    r.POST("/users", func(c *gin.Context) {
        var user User
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(200, user)
    })
    
    r.Run()
}

// 验证函数
func validatePhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    // 中国大陆手机号验证
    matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
    return matched
}

2.2 验证函数签名 #

go
// validator.FieldLevel 接口
type FieldLevel interface {
    Field() reflect.Value       // 获取当前字段值
    FieldName() string          // 获取字段名
    StructFieldName() string    // 获取结构体字段名
    Param() string              // 获取验证参数
    GetStructFieldOK() (reflect.Value, bool) // 获取其他字段
    Top() reflect.Value         // 获取顶层结构体
    Parent() reflect.Value      // 获取父结构体
}

三、常用自定义验证器 #

3.1 手机号验证 #

go
func validatePhone(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
    return matched
}

func main() {
    r := gin.Default()
    
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("phone", validatePhone)
    }
    
    r.Run()
}

3.2 身份证号验证 #

go
func validateIDCard(fl validator.FieldLevel) bool {
    idCard := fl.Field().String()
    
    // 18位身份证号验证
    if len(idCard) != 18 {
        return false
    }
    
    // 验证格式
    matched, _ := regexp.MatchString(`^\d{17}[\dXx]$`, idCard)
    if !matched {
        return false
    }
    
    // 验证校验码
    weights := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
    checkCodes := "10X98765432"
    
    sum := 0
    for i := 0; i < 17; i++ {
        sum += int(idCard[i]-'0') * weights[i]
    }
    
    checkCode := checkCodes[sum%11]
    return strings.ToUpper(string(idCard[17])) == string(checkCode)
}

func main() {
    r := gin.Default()
    
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("idcard", validateIDCard)
    }
    
    r.Run()
}

3.3 用户名验证 #

go
func validateUsername(fl validator.FieldLevel) bool {
    username := fl.Field().String()
    
    // 4-20位,字母开头,只含字母数字下划线
    matched, _ := regexp.MatchString(`^[a-zA-Z][a-zA-Z0-9_]{3,19}$`, username)
    return matched
}

func main() {
    r := gin.Default()
    
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("username", validateUsername)
    }
    
    r.Run()
}

3.4 密码强度验证 #

go
func validatePassword(fl validator.FieldLevel) bool {
    password := fl.Field().String()
    
    // 至少8位,包含大小写字母和数字
    if len(password) < 8 {
        return false
    }
    
    hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
    hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
    hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password)
    
    return hasUpper && hasLower && hasDigit
}

func main() {
    r := gin.Default()
    
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("strong_password", validatePassword)
    }
    
    r.Run()
}

3.5 日期范围验证 #

go
func validateDateRange(fl validator.FieldLevel) bool {
    dateStr := fl.Field().String()
    
    // 解析日期
    date, err := time.Parse("2006-01-02", dateStr)
    if err != nil {
        return false
    }
    
    // 获取参数
    param := fl.Param()
    if param == "" {
        return true
    }
    
    // 检查是否在指定范围内
    switch param {
    case "past":
        return date.Before(time.Now())
    case "future":
        return date.After(time.Now())
    default:
        return true
    }
}

func main() {
    r := gin.Default()
    
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("date_range", validateDateRange)
    }
    
    r.Run()
}

3.6 枚举验证 #

go
func validateEnum(fl validator.FieldLevel) bool {
    value := fl.Field().String()
    param := fl.Param()
    
    // 参数格式: "value1,value2,value3"
    allowedValues := strings.Split(param, ",")
    
    for _, v := range allowedValues {
        if value == v {
            return true
        }
    }
    
    return false
}

func main() {
    r := gin.Default()
    
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("enum", validateEnum)
    }
    
    r.Run()
}

// 使用
type Request struct {
    Status string `binding:"required,enum=active,inactive,pending"`
}

四、跨字段验证 #

4.1 字段比较验证 #

go
func validateFieldEq(fl validator.FieldLevel) bool {
    // 获取当前字段
    field := fl.Field()
    
    // 获取参数(要比较的字段名)
    otherField := fl.Param()
    
    // 获取其他字段
    otherValue, _, ok := fl.GetStructFieldOK(otherField)
    if !ok {
        return false
    }
    
    // 比较
    switch field.Kind() {
    case reflect.String:
        return field.String() == otherValue.String()
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return field.Int() == otherValue.Int()
    default:
        return false
    }
}

4.2 条件验证 #

go
func validateRequiredIf(fl validator.FieldLevel) bool {
    // 获取参数:field=value
    param := fl.Param()
    parts := strings.Split(param, "=")
    if len(parts) != 2 {
        return false
    }
    
    otherField := parts[0]
    expectedValue := parts[1]
    
    // 获取其他字段值
    otherValue, _, ok := fl.GetStructFieldOK(otherField)
    if !ok {
        return true // 其他字段不存在,跳过验证
    }
    
    // 如果其他字段等于期望值,当前字段必填
    if otherValue.String() == expectedValue {
        return !fl.Field().IsZero()
    }
    
    return true
}

五、验证器管理 #

5.1 集中注册 #

go
package validator

import (
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

func InitValidators() {
    v, ok := binding.Validator.Engine().(*validator.Validate)
    if !ok {
        return
    }
    
    // 注册所有自定义验证器
    v.RegisterValidation("phone", validatePhone)
    v.RegisterValidation("idcard", validateIDCard)
    v.RegisterValidation("username", validateUsername)
    v.RegisterValidation("strong_password", validatePassword)
    v.RegisterValidation("date_range", validateDateRange)
    v.RegisterValidation("enum", validateEnum)
    
    // 注册结构体级别验证
    v.RegisterStructValidation(validateStruct, User{})
}

// main.go
func main() {
    validator.InitValidators()
    
    r := gin.Default()
    r.Run()
}

5.2 验证器配置 #

go
type ValidatorConfig struct {
    validators map[string]validator.Func
}

func NewValidatorConfig() *ValidatorConfig {
    return &ValidatorConfig{
        validators: make(map[string]validator.Func),
    }
}

func (c *ValidatorConfig) Register(name string, fn validator.Func) {
    c.validators[name] = fn
}

func (c *ValidatorConfig) Apply() {
    v, ok := binding.Validator.Engine().(*validator.Validate)
    if !ok {
        return
    }
    
    for name, fn := range c.validators {
        v.RegisterValidation(name, fn)
    }
}

func main() {
    config := NewValidatorConfig()
    
    config.Register("phone", validatePhone)
    config.Register("idcard", validateIDCard)
    config.Apply()
    
    r := gin.Default()
    r.Run()
}

六、验证错误处理 #

6.1 自定义错误消息 #

go
var errorMessages = map[string]string{
    "phone":           "手机号格式不正确",
    "idcard":          "身份证号格式不正确",
    "username":        "用户名必须是4-20位字母开头,只含字母数字下划线",
    "strong_password": "密码至少8位,包含大小写字母和数字",
}

func getCustomErrorMsg(fe validator.FieldError) string {
    if msg, ok := errorMessages[fe.Tag()]; ok {
        return msg
    }
    return fe.Error()
}

func main() {
    r := gin.Default()
    
    r.POST("/users", func(c *gin.Context) {
        var user User
        if err := c.ShouldBindJSON(&user); err != nil {
            var verr validator.ValidationErrors
            if errors.As(err, &verr) {
                errors := make(map[string]string)
                for _, fe := range verr {
                    errors[fe.Field()] = getCustomErrorMsg(fe)
                }
                c.JSON(400, gin.H{"errors": errors})
                return
            }
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(200, user)
    })
    
    r.Run()
}

七、完整示例 #

7.1 用户注册验证 #

go
package main

import (
    "errors"
    "regexp"
    "strings"
    
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

type RegisterRequest struct {
    Username string `json:"username" binding:"required,username"`
    Password string `json:"password" binding:"required,strong_password"`
    Phone    string `json:"phone" binding:"required,phone"`
    Email    string `json:"email" binding:"required,email"`
    IDCard   string `json:"idCard" binding:"omitempty,idcard"`
}

func main() {
    r := gin.Default()
    
    // 注册验证器
    initValidators()
    
    r.POST("/register", func(c *gin.Context) {
        var req RegisterRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            handleValidationError(c, err)
            return
        }
        
        c.JSON(200, gin.H{"message": "注册成功"})
    })
    
    r.Run()
}

func initValidators() {
    v, ok := binding.Validator.Engine().(*validator.Validate)
    if !ok {
        return
    }
    
    v.RegisterValidation("phone", validatePhone)
    v.RegisterValidation("idcard", validateIDCard)
    v.RegisterValidation("username", validateUsername)
    v.RegisterValidation("strong_password", validatePassword)
}

func handleValidationError(c *gin.Context, err error) {
    var verr validator.ValidationErrors
    if errors.As(err, &verr) {
        errors := make(map[string]string)
        for _, fe := range verr {
            errors[fe.Field()] = getErrorMsg(fe)
        }
        c.JSON(400, gin.H{"errors": errors})
        return
    }
    c.JSON(400, gin.H{"error": err.Error()})
}

func getErrorMsg(fe validator.FieldError) string {
    fieldNames := map[string]string{
        "Username": "用户名",
        "Password": "密码",
        "Phone":    "手机号",
        "Email":    "邮箱",
        "IDCard":   "身份证号",
    }
    
    field := fieldNames[fe.Field()]
    
    switch fe.Tag() {
    case "required":
        return field + "不能为空"
    case "phone":
        return "手机号格式不正确"
    case "idcard":
        return "身份证号格式不正确"
    case "username":
        return "用户名必须是4-20位字母开头"
    case "strong_password":
        return "密码至少8位,包含大小写字母和数字"
    case "email":
        return "邮箱格式不正确"
    default:
        return field + "验证失败"
    }
}

八、总结 #

8.1 核心要点 #

要点 说明
注册验证器 RegisterValidation
验证函数 func(FieldLevel) bool
获取字段值 fl.Field()
获取参数 fl.Param()
跨字段 fl.GetStructFieldOK()

8.2 最佳实践 #

实践 说明
集中管理 统一注册验证器
错误消息 提供友好的错误提示
复用验证 封装常用验证规则
测试覆盖 编写单元测试

8.3 下一步 #

现在你已经掌握了自定义验证,接下来让我们学习 错误处理,了解Gin的错误处理机制!

最后更新:2026-03-28