Go 请求验证

Go 服务中请求数据验证方案,涵盖手动验证、go-playground/validator 结构体标签、自定义校验器与错误格式化。

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

Go 请求验证

一句话定义

Go 请求验证是在 HTTP Handler 入口处对请求参数进行合法性校验的工程实践,go-playground/validator 通过结构体标签声明式定义规则,是 Go 生态中最主流的验证方案。

核心机制 / 工作原理

手动验证模式

type CreateUserRequest struct {
    Name  string
    Email string
    Age   int
}

func (r *CreateUserRequest) Validate() error {
    if r.Name == "" {
        return errors.New("name is required")
    }
    if len(r.Name) > 100 {
        return errors.New("name must be at most 100 characters")
    }
    if !strings.Contains(r.Email, "@") {
        return errors.New("invalid email format")
    }
    if r.Age < 0 || r.Age > 150 {
        return errors.New("age must be between 0 and 150")
    }
    return nil
}

灵活但冗长,容易遗漏字段。

go-playground/validator(声明式验证)

import "github.com/go-playground/validator/v10"

type CreateUserRequest struct {
    Name     string `json:"name"     validate:"required,max=100"`
    Email    string `json:"email"    validate:"required,email"`
    Age      int    `json:"age"      validate:"gte=0,lte=150"`
    Password string `json:"password" validate:"required,min=8,max=72"`
    Role     string `json:"role"     validate:"oneof=admin user guest"`
}

var validate = validator.New()

func handler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)

    if err := validate.Struct(req); err != nil {
        // 返回结构化错误
        formatValidationErrors(w, err)
        return
    }
    // 校验通过,继续处理
}

常用内置标签

  • required — 非零值
  • email / url / uuid — 格式校验
  • min=N / max=N — 最小/最大长度或数值
  • oneof=a b c — 枚举值
  • len=N — 精确长度
  • startswith=prefix / contains=sub — 字符串规则
  • dive — 对切片/Map 中的每个元素递归校验

自定义校验器

// 注册自定义校验:用户名只能包含字母和数字
validate.RegisterValidation("alphanum_under", func(fl validator.FieldLevel) bool {
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, fl.Field().String())
    return matched
})

// 结构体中使用
type User struct {
    Username string `validate:"required,alphanum_under,min=3,max=30"`
}

嵌套结构体验证

type Address struct {
    City   string `validate:"required"`
    ZipCode string `validate:"required,len=5,numeric"`
}

type User struct {
    Name    string  `validate:"required"`
    Address Address `validate:"required"` // 嵌套结构体自动递归验证
    Tags    []string `validate:"required,dive,required,min=1"` // dive: 逐元素校验
}

错误格式化

func formatValidationErrors(w http.ResponseWriter, err error) {
    errs := err.(validator.ValidationErrors)
    messages := make(map[string]string)
    for _, e := range errs {
        messages[e.Field()] = fmt.Sprintf("failed on '%s' tag", e.Tag())
    }
    w.WriteHeader(http.StatusBadRequest)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "errors": messages,
    })
}

最小例子 / 最小场景

var validate = validator.New()

type LoginRequest struct {
    Email    string `json:"email"    validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    var req LoginRequest
    json.NewDecoder(r.Body).Decode(&req)
    if err := validate.Struct(req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // 认证逻辑...
}

为什么重要

  • 防御第一层:验证是 API 安全的第一道防线,防止非法数据进入业务逻辑和数据库。
  • 声明式清晰:结构体标签让验证规则与数据定义共存,代码即文档。
  • 统一错误格式:集中处理验证错误,返回前端友好的结构化响应。
  • 减少 boilerplate:相比手写 if-else,标签声明减少 80% 以上的验证代码。

边界与易混淆点

  • 验证 vs 业务规则:validator 适合通用格式校验(邮箱格式、长度范围)。业务规则(“邮箱是否已注册”)应在 Service 层处理,不要塞进校验标签。
  • 零值陷阱requiredint 类型无效——Go 的零值 0 不会被视为”未设置”。使用指针 *intrequired_if 标签解决。
  • 自定义错误消息:validator 默认错误信息对用户不友好。生产环境应注册自定义翻译器(ut 包)或手动映射。
  • 性能考量validator.New() 应全局创建一次(内部缓存反射信息),不要在每次请求中新建。
  • JSON tag 和 validate tag 的区别json 控制序列化,validate 控制校验,两者独立。json:"-" 不影响 validate:"required" 的行为。
  • dive 标签:验证切片元素时必须加 dive,否则只校验切片本身(非空),不校验元素内容。这是最常见的遗漏。
创建于 2026/6/25 更新于 2026/6/25