前言 在 Gin 中实现一个自定义验证器,本质上就是 实现一个函数 + 注册它 + 在结构体标签里使用它 。 Validator 底层用的是 go-playground/validator/v10,所以整个流程也完全遵循该库的规则。
1. 实现一个验证器函数 要写一个验证器,说白了就是写一个回调函数,这个函数最终会被 Gin 的 Validator 调用。 它的签名非常固定:
1 func (fl validator.FieldLevel) bool
fl 是 FieldLevel 对象,包含了关于当前字段的大量信息:
Field():字段的值(Value)
FieldName():字段名称
Param():标签里传进来的参数
GetTag():整个验证器 tag 字符串
……
我们所要实现的验证器只需要根据自己的业务逻辑返回 true(通过)或 false(失败)。
示例:名称必须以 alex- 作为前缀 1 2 3 4 5 6 7 8 func nameValidator (fl validator.FieldLevel) bool { if name, ok := fl.Field().Interface().(string ); ok { if strings.HasPrefix(name, "alex-" ) { return true } } return false }
这里做了两件事:
用 fl.Field().Interface() 拿到字段原始值
判断它是否是 string 并检查前缀
非常简单,逻辑也清晰。
2. 注册验证器 实现了验证器以后,就必须显式注册它,否则 Gin 永远不会调用它。
注册流程:
通过 binding.Validator.Engine() 拿到底层的 validator.Validate 实例
调用 RegisterValidation 注册
1 2 3 if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("nameValid" , nameValidator) }
"nameValid" 是给验证器起的名字
nameValidator 是刚才实现的函数
之后就可以直接在结构体标签中使用 "nameValid" 了。
3. 在结构体上使用自定义验证器 用法和内置的 validator 标签完全一样,只要写在 binding 字段中:
1 2 3 type User struct { Name string `form:"name" binding:"required,nameValid"` }
required:必填
nameValid:你自定义的验证器
当用户发起请求时,Gin 的绑定器会自动调用验证器。
4. 完整代码 如下是一个完整示例,看起来更直观。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "strings" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" )type User struct { Name string `form:"name" binding:"required,nameValid"` }func nameValidator (fl validator.FieldLevel) bool { if name, ok := fl.Field().Interface().(string ); ok { if strings.HasPrefix(name, "alex-" ) { return true } } return false }func main () { r := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("nameValid" , nameValidator) } r.GET("/user" , func (c *gin.Context) { var user User if err := c.ShouldBind(&user); err == nil { c.JSON(200 , gin.H{"message" : "Validation passed!" }) } else { c.JSON(400 , gin.H{"error" : err.Error()}) } }) r.Run() }
请求示例 可以看到,名称前缀没有 alex- 时请求失败,加上 alex- 请求成功。
官方示例 Gin 官方示例展示了一个 日期必须大于今天 的自定义验证器。 这个验证器稍微复杂一点,也会用到 Gin 的时间格式化能力。
在 Go 中,时间格式化非常特别,它不是使用 YYYY-MM-DD,而是直接用这个固定日期:
这是 Go 语言的一个历史设计,也是一种“记忆法”。
所以:
想用 YYYY-MM-DD → 必须写成 2006-01-02
想用 YYYY/MM/DD → 必须写 2006/01/02
想解析时间戳 → 直接用 unix, unixmilli, unixmicro, unixnano
Gin 的 binding 只是把这些格式抽象成 tag。
实现日期可预约验证器
1 2 3 4 5 6 7 8 9 10 func bookableDate (fl validator.FieldLevel) bool { date, ok := fl.Field().Interface().(time.Time) if ok { today := time.Now() if today.After(date) { return false } } return true }
完整官方示例
gtfield=CheckIn:CheckOut 必须大于 CheckIn
time_format:"2006-01-02":binding 会自动解析 CheckIn、CheckOut
bookabledate 验证时间是否在未来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" )type Booking struct { CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` CheckOUt time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"` }func bookableDate (fl validator.FieldLevel) bool { date, ok := fl.Field().Interface().(time.Time) if ok { today := time.Now() if today.After(date) { return false } } return true }func main () { r := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("bookabledate" , bookableDate) } r.GET("/bookable" , func (c *gin.Context) { var b Booking if err := c.ShouldBind(&b); err == nil { c.JSON(200 , gin.H{"message" : "Booking dates are valid!" }) } else { c.JSON(400 , gin.H{"error" : err.Error()}) } }) r.Run() }