← Zap | Cobra →

Validator - Go 参数校验库

go-playground/validator 是 Go 最流行的参数校验库,支持丰富的校验规则和自定义验证。掌握 Validator 是开发安全、健壮的 Go Web 应用的基础。

快速开始

📝 基础校验

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type User struct {
    Username string `validate:"required,min=3,max=20"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"required,min=18,max=100"`
    Password string `validate:"required,min=8,containsany=!@#$%"`
}

func main() {
    validate := validator.New()
    
    user := User{
        Username: "alice",
        Email:    "alice@example.com",
        Age:      25,
        Password: "pass123!",
    }
    
    err := validate.Struct(user)
    if err != nil {
        // 字段错误
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Printf("Field: %s, Error: %s\n", err.Field(), err.Tag())
        }
        return
    }
    
    fmt.Println("Validation passed!")
}

💡 Validator 要点

  • 单例模式: validate 实例应全局复用
  • 并发安全: Validate 是并发安全的
  • 缓存: 结构体校验规则会被缓存
  • 嵌套校验: 支持嵌套结构体和切片

内置校验规则

📖 常用校验规则

type Request struct {
    // 必填
    Name     string `validate:"required"`
    Nickname string `validate:"omitempty,min=2"` // 可选,但有值时需满足
    
    // 字符串
    Email    string `validate:"email"`
    URL      string `validate:"url"`
    Mobile   string `validate:"mobile"`
    IP       string `validate:"ip"`
    UUID     string `validate:"uuid"`
    
    // 长度
    Code     string `validate:"len=6"`       // 固定长度
    Password string `validate:"min=8,max=20"` // 长度范围
    
    // 数值
    Age      int    `validate:"min=0,max=150"`
    Score    float64 `validate:"gte=0,lte=100"` // >= 和 <=
    Count    int    `validate:"gt=0"`        // >
    
    // 正则
    ZipCode  string `validate:"regexp=^[0-9]{6}$"`
    
    // 枚举
    Status   string `validate:"oneof=active inactive pending"`
    Role     string `validate:"oneof=admin user guest"`
    
    // 时间
    Birthday time.Time `validate:"lt"` // 必须小于当前时间
    EndTime  time.Time `validate:"gtfield=StartTime"` // 必须大于 StartTime 字段
    
    // 切片/数组
    Tags     []string `validate:"min=1,max=5"`
    IDs      []int    `validate:"unique"` // 元素唯一
    
    // 嵌套
    Address  *Address `validate:"required,dive"` // dive 深入嵌套
    Items    []Item   `validate:"dive"`
}

type Address struct {
    Province string `validate:"required"`
    City     string `validate:"required"`
    District string `validate:"required"`
}

type Item struct {
    ID    int    `validate:"required"`
    Name  string `validate:"required"`
    Price float64 `validate:"gt=0"`
}

错误处理

📝 解析校验错误

func validateUser(user User) map[string]string {
    errors := make(map[string]string)
    
    err := validate.Struct(user)
    if err == nil {
        return errors
    }
    
    for _, err := range err.(validator.ValidationErrors) {
        field := err.Field()
        tag := err.Tag()
        
        switch tag {
        case "required":
            errors[field] = "不能为空"
        case "email":
            errors[field] = "邮箱格式不正确"
        case "min":
            errors[field] = fmt.Sprintf("长度不能小于 %s", err.Param())
        case "max":
            errors[field] = fmt.Sprintf("长度不能超过 %s", err.Param())
        default:
            errors[field] = "校验失败"
        }
    }
    
    return errors
}

自定义验证

📝 注册自定义规则

package main

import (
    "github.com/go-playground/validator/v10"
    "regexp"
)

var validate *validator.Validate

func init() {
    validate = validator.New()
    
    // 自定义规则:校验身份证号
    validate.RegisterValidation("id_card", func(fl validator.FieldLevel) bool {
        idCard := fl.Field().String()
        matched, _ := regexp.MatchString(`^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$`, idCard)
        return matched
    })
    
    // 自定义规则:校验密码强度
    validate.RegisterValidation("password_strength", func(fl validator.FieldLevel) bool {
        password := fl.Field().String()
        if len(password) < 8 {
            return false
        }
        // 必须包含字母和数字
        hasLetter := regexp.MatchString(`[a-zA-Z]`, password)
        hasNumber := regexp.MatchString(`[0-9]`, password)
        return hasLetter && hasNumber
    })
}

type RegisterRequest struct {
    IDCard   string `validate:"required,id_card"`
    Password string `validate:"required,password_strength"`
}

与 Gin 集成

📝 Gin + Validator 中间件

package middleware

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

type ErrorResponse struct {
    Code    int               `json:"code"`
    Message string            `json:"message"`
    Errors  map[string]string `json:"errors,omitempty"`
}

func ValidationMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 获取 gin 的 validator
        v, _ := c.Get("validator")
        validate, ok := v.(*validator.Validate)
        if !ok {
            c.Next()
            return
        }
        
        // 获取请求体结构体
        if c.Request.Body == nil {
            c.Next()
            return
        }
        
        c.Next()
    }
}

func main() {
    r := gin.Default()
    
    // 替换 gin 的 validator
    if v, ok := r.Validator().Engine().(*validator.Validate); ok {
        // 注册自定义规则
        v.RegisterValidation("custom_rule", func(fl validator.FieldLevel) bool {
            return true
        })
    }
    
    // 使用示例
    r.POST("/users", func(c *gin.Context) {
        var req CreateUserRequest
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(http.StatusBadRequest, ErrorResponse{
                Code:    400,
                Message: "Validation failed",
                Errors:  formatErrors(err),
            })
            return
        }
        
        c.JSON(http.StatusOK, gin.H{"message": "success"})
    })
    
    r.Run(":8080")
}

func formatErrors(err error) map[string]string {
    errors := make(map[string]string)
    for _, e := range err.(validator.ValidationErrors) {
        errors[e.Field()] = getErrorMessage(e)
    }
    return errors
}

最佳实践

✅ Validator 使用建议

  • 单例模式: 全局复用 validator 实例
  • 自定义错误: 将英文错误转换为中文
  • 分组校验: 不同场景使用不同校验组
  • 嵌套校验: 使用 dive 深入嵌套结构
  • 性能考虑: 避免过度复杂的正则