Go 请求验证
Go 服务中请求数据验证方案,涵盖手动验证、go-playground/validator 结构体标签、自定义校验器与错误格式化。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 服务工程
- 前置概念: Go Context
- 并列概念: Go 认证与 JWT, Go 日志
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 层处理,不要塞进校验标签。
- 零值陷阱:
required对int类型无效——Go 的零值0不会被视为”未设置”。使用指针*int或required_if标签解决。 - 自定义错误消息:validator 默认错误信息对用户不友好。生产环境应注册自定义翻译器(
ut包)或手动映射。 - 性能考量:
validator.New()应全局创建一次(内部缓存反射信息),不要在每次请求中新建。 - JSON tag 和 validate tag 的区别:
json控制序列化,validate控制校验,两者独立。json:"-"不影响validate:"required"的行为。 dive标签:验证切片元素时必须加dive,否则只校验切片本身(非空),不校验元素内容。这是最常见的遗漏。