绑定验证器

Gin 使用 validator 库进行参数验证,支持丰富的验证规则。掌握高级用法可以处理复杂的验证场景。

内置验证规则

type User struct {
    Name     string `json:"name" binding:"required,min=2,max=50"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=8,max=20"`
    Age      int    `json:"age" binding:"required,gte=0,lte=150"`
    Phone    string `json:"phone" binding:"omitempty,len=11"`
    Website  string `json:"website" binding:"omitempty,url"`
    Role     string `json:"role" binding:"required,oneof=admin user guest"`
}

常用规则:

  • required - 必填
  • min,max - 字符串长度或数值范围
  • gte,lte - 大于等于、小于等于
  • gt,lt - 大于、小于
  • email - 邮箱格式
  • url - URL 格式
  • oneof - 枚举值
  • omitempty - 可选

自定义验证器

注册自定义验证规则:

package main

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

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

func main() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("phone", validatePhone)
    }
    
    r := gin.Default()
    
    r.POST("/user", func(c *gin.Context) {
        var user struct {
            Name  string `json:"name" binding:"required"`
            Phone string `json:"phone" binding:"required,phone"`
        }
        
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(http.StatusOK, gin.H{"message": "ok"})
    })
    
    r.Run(":8080")
}

结构体级别验证

验证多个字段的关联关系:

type RegisterForm struct {
    Password        string `json:"password" binding:"required,min=8"`
    ConfirmPassword string `json:"confirm_password" binding:"required"`
}

func validateRegister(sl validator.StructLevel) {
    form := sl.Current().Interface().(RegisterForm)
    
    if form.Password != form.ConfirmPassword {
        sl.ReportError(form.ConfirmPassword, "confirm_password", "ConfirmPassword", "eqfield", "")
    }
}

func main() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterStructValidation(validateRegister, RegisterForm{})
    }
    
    r := gin.Default()
    
    r.POST("/register", func(c *gin.Context) {
        var form RegisterForm
        if err := c.ShouldBindJSON(&form); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(http.StatusOK, gin.H{"message": "ok"})
    })
    
    r.Run(":8080")
}

自定义错误消息

var validationMessages = map[string]string{
    "required": "此字段为必填项",
    "email":    "邮箱格式不正确",
    "min":      "长度不能少于%s",
    "max":      "长度不能超过%s",
    "phone":    "手机号格式不正确",
}

func getErrorMessage(err error) string {
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, e := range validationErrors {
            if msg, exists := validationMessages[e.Tag()]; exists {
                if e.Param() != "" {
                    return fmt.Sprintf(msg, e.Param())
                }
                return msg
            }
        }
    }
    return "参数验证失败"
}

r.POST("/user", func(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": getErrorMessage(err),
        })
        return
    }
    c.JSON(http.StatusOK, user)
})

条件验证

type Order struct {
    Type       string `json:"type" binding:"required,oneof=physical digital"`
    Address    string `json:"address" binding:"required_if=Type physical"`
    DownloadURL string `json:"download_url" binding:"required_if=Type digital"`
}

r.POST("/order", func(c *gin.Context) {
    var order Order
    if err := c.ShouldBindJSON(&order); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, order)
})

嵌套结构体验证

type Address struct {
    Province string `json:"province" binding:"required"`
    City     string `json:"city" binding:"required"`
    Street   string `json:"street" binding:"required"`
}

type User struct {
    Name    string  `json:"name" binding:"required"`
    Address Address `json:"address" binding:"required"`
}

r.POST("/user", func(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, user)
})

切片验证

type CreateOrder struct {
    Items []OrderItem `json:"items" binding:"required,min=1,dive"`
}

type OrderItem struct {
    ProductID int `json:"product_id" binding:"required"`
    Quantity  int `json:"quantity" binding:"required,gte=1"`
}

r.POST("/order", func(c *gin.Context) {
    var order CreateOrder
    if err := c.ShouldBindJSON(&order); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, order)
})

自定义字段名

func main() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterTagNameFunc(func(fld reflect.StructField) string {
            name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
            if name == "-" {
                return ""
            }
            return name
        })
    }
    
    r := gin.Default()
    r.Run(":8080")
}

小结

Gin 的验证器功能强大,支持自定义规则、结构体级别验证、条件验证等。自定义错误消息可以提升用户体验。合理使用验证规则可以减少业务代码中的参数检查逻辑。