密码复杂度校验器必须在 gin.default() 后立即注册,否则因 validator 懒加载而失效;重复注册同名 tag 会 panic,需用 hasregisterednamespace 防御;tag 区分大小写;函数签名必须为 func(fl validator.fieldlevel) bool,仅基于当前字段值校验。

注册密码复杂度校验器必须在 gin.Default() 之后立即执行
自定义校验器不生效,90% 是因为注册时机错误。Gin 的 validator 实例是懒加载的,binding.Validator.Engine() 在首次调用 c.ShouldBind() 前返回 nil;若在 init() 函数或 handler 里注册,函数根本不会被调用。
正确做法是在 main() 开头、r := gin.Default() 后立刻注册:
func main() {
r := gin.Default()
// 必须放这里!不能放 init(),不能放路由 handler 里
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("password_complexity", passwordComplexityFunc)
}
r.POST("/login", loginHandler)
_ = r.Run()
}
- 重复注册同名 tag(如两次
"password_complexity")会 panic,建议加if !v.HasRegisteredNamespace("password_complexity")防御 - tag 名区分大小写:
binding:"password_complexity"和binding:"PasswordComplexity"是两个不同规则 - Gin v1.9+ 已内置
validator/v10,不要手动go get,否则版本冲突导致panic: interface conversion: interface {} is nil
passwordComplexityFunc 必须返回 bool,且只操作 fl.Field()
validator 要求函数签名严格为 func(fl validator.FieldLevel) bool。它不接受 error、不支持上下文、也不能访问其他字段——所有逻辑必须从当前字段值推导。
常见错误是试图取 fl.Parent() 或调用数据库查唯一性,这属于结构体级校验范畴,此处不适用。
示例:要求密码含数字、大写字母、小写字母、特殊字符(*|&$#)中至少 3 类:
func passwordComplexityFunc(fl validator.FieldLevel) bool {
s := fl.Field().String()
if len(s) = 3
}
- 别用
regexp.MustCompile在每次校验时编译正则——它非并发安全,且开销大;应提前全局编译或改用字符遍历 - 空字符串或非 string 类型字段(如
int)需先做类型断言,否则fl.Field().String()返回空串,导致误判 - 长度检查(如
min=8)建议仍用内置 tag,和自定义逻辑解耦,避免重复逻辑
绑定时用 binding:"required,password_complexity",不是函数名
struct tag 里填的是注册时传入的 **第一个参数**(即规则名),不是函数变量名。写成 binding:"required,myPasswordCheck" 却注册了 "password_complexity",校验器完全不会触发。
典型结构体写法:
type LoginReq struct {
Username string `json:"username" binding:"required,min=3,max=20"`
Password string `json:"password" binding:"required,password_complexity"`
}
- 多个校验规则用英文逗号连接,顺序无关,但
required建议放最前——避免空值进入自定义函数造成 panic - 如果字段是
pointer类型(如*string),fl.Field().String()会 panic,必须先判空:if !fl.Field().IsValid() || fl.Field().Kind() == reflect.Ptr && fl.Field().IsNil() - 校验失败时默认报错是
Key: 'LoginReq.Password' Error: Field validation for 'Password' failed on the 'password_complexity' tag,要替换为中文需额外配置翻译器,不是改函数能解决的
结构体级校验才适合跨字段或 DB 查询类逻辑
密码确认(RePassword 与 Password 一致)、用户名唯一性、邮箱未注册等,都不能塞进字段级 password_complexity 函数里——validator 字段级函数看不到其他字段,更无法 IO。
这类需求要用结构体级验证器:
func registerStructLevelValidation(sl validator.StructLevel) {
u := sl.Current().Interface().(RegisterReq)
if u.Password != u.RePassword {
sl.ReportError(u.Password, "Password", "Password", "eqfield", "RePassword")
}
// 这里可查 DB,但注意:别在 validator 里做耗时操作,应提前缓存或异步处理
}
然后注册:v.RegisterStructValidation(registerStructLevelValidation, RegisterReq{})。
真正容易被忽略的是:字段级和结构体级验证器的执行顺序。Gin 先跑所有字段级(包括 password_complexity),再跑结构体级。所以即使密码格式不对,也会继续校验确认密码是否匹配——除非你主动在字段级返回 false 阻断流程。











