go-playground/validator/v10是通用验证函数的最终形态,底层用反射但无需手动操作,已优化缓存机制并覆盖常见坑点,推荐直接使用而非手写。

直接用 go-playground/validator/v10,别自己手写反射循环
绝大多数场景下,go-playground/validator/v10 就是通用验证函数的最终形态——它底层用反射,但你完全不用碰 reflect.Value。自己从零实现字段遍历、标签解析、类型分支判断,90% 的坑(比如 time.Time 零值、*string 为 nil、[]int 空切片 vs nil)都会重踩一遍。
关键不是“能不能写”,而是“值不值得维护”。validator 已把反射开销压到初始化阶段:第一次 Validate.Struct() 时缓存校验链,之后全是普通函数调用。
- 安装:
go get github.com/go-playground/validator/v10 - 结构体字段加
validate标签,不是json或gorm - 必须显式调
validate.Struct(&v),不会自动触发 - 错误类型是
validator.ValidationErrors,需类型断言才能遍历
struct tag 写错一个空格,规则就静默失效
validate:"required,min=3,max=20" 合法;validate:"required, min=3"(逗号后多空格)会导致 min 被跳过,且 validator 不报错——这是线上最常卡住的点。
所有规则用英文逗号分隔,逗号前后绝对不能有空格。嵌套字段用点号,比如 Address.Street,但 tag 要写在 Address 字段上,而不是 Street 上。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
required对*string只判指针是否为 nil,不检查解引用后的值 -
required对[]string只判是否为 nil,[]string{}(空 slice)会通过 - 要校验 slice 长度,必须配
min=1或dive(如validate:"min=1,dive,required") -
dive必须紧跟在 slice/map tag 后,validate:"required,dive"无效
time.Time、int、[]string 这三类最容易漏检
time.Time 的零值是 0001-01-01,required 永远通过;int 零值是 0,required 同样无效;[]string 的空切片是合法值,required 不报错——这些都不是 validator 的 bug,而是它们的语义设计如此。
业务上真正想表达的,往往是“非零时间”“大于 0 的整数”“至少含一个元素”,得换规则:
-
time.Time改用datetime=2006-01-02或自定义函数判是否为零时间 -
int改用gt=0或ne=0,别依赖required -
[]string显式加min=1,或用dive配子项规则 - 嵌套 struct 中字段为 nil 时,外层
required不会递归触发内层校验,得手动调validate.Struct()
性能敏感?提前编译校验器能省 30%+ 开销
默认每次 Validate.Struct() 都会走一次反射扫描。如果结构体固定、调用量大(比如 API 网关层),可以用 validator.RegisterValidation + validate.StructC 提前编译。
更简单的是复用单例 validator 实例,并在服务启动时对常用结构体预热一次:validate.Struct(&dummyUser),让缓存就位。
- 避免在请求处理中反复
validator.New(),实例可全局复用 - 自定义规则注册只需一次,在
init()或 main 开头完成 - gin 框架中替换默认校验器,必须在任何路由注册前执行
engine.Validator = yourValidator
真正难的不是写对那几行 tag,而是理解 validator 只校验它能安全读取的字段——比如未导出字段(小写字母开头)永远不参与校验,哪怕你写了 tag。

















