validator/v10 默认不支持多字段联合唯一校验,因其tag机制仅作用于单个字段,且内置规则无法跨字段组合或访问数据库;需通过闭包封装DB依赖注册自定义验证函数,并配合数据库联合唯一索引兜底。

为什么 validator 默认不支持多字段联合唯一校验
validator/v10 的 tag 机制只作用于单个字段,unique、unique-field 这类常见扩展也仅限于结构体内字段互查,无法跨表或调 DB 判断「用户名+邮箱」组合是否已存在。你写 binding:"unique_fields=username,email" 是无效的——validator 根本不认识这个 tag,也不会自动查数据库。
注册自定义验证器时必须传入 DB 或 Repository 实例
联合唯一性本质是业务规则,必须依赖外部状态(比如用户表),所以验证函数不能是纯函数,得能访问数据层。常见错误是把 DB 句柄塞进 init() 或全局变量,导致并发下错乱或测试难 mock。
- 推荐做法:用闭包封装依赖,例如
RegisterValidation("user_email_unique", NewUserEmailUniqueValidator(db)) -
NewUserEmailUniqueValidator返回func(fl validator.FieldLevel) bool,内部用传入的db查SELECT COUNT(*) FROM users WHERE username = ? AND email = ? - 别在验证器里做 INSERT 或事务控制——校验阶段只读,冲突处理交给业务 handler
c.ShouldBind() 前必须确保 DB 连接已就绪且可复用
如果你的验证器依赖 *sql.DB,而它是在 main() 启动时初始化的,那没问题;但若用的是连接池(如 pgxpool)或带 context 超时的 client,就得确认它能在 validator 调用时正常执行查询。常见失败现象是 panic: driver: bad connection 或超时返回 false。
- 验证器中所有 DB 操作应设短超时(如 500ms),避免阻塞整个请求绑定流程
- 不要在验证函数里用
context.Background()——应从fl提取上下文,或由闭包传入 request-scoped context - 单元测试时,直接传入 mock db(如 sqlmock),断言是否执行了预期的 SELECT
别把联合唯一校验和业务插入逻辑耦合在同一个事务里
很多人想“先校验再插入”,结果在 handler 里开事务、查、插、commit——这有竞态:A 请求校验通过,B 请求也校验通过,然后 A 插入,B 插入时违反唯一索引,报 duplicate key。这不是 validator 的问题,而是设计没兜住。
- 正确姿势:DB 层建联合唯一索引(
UNIQUE(username, email)),校验只是前置友好提示,最终以数据库约束为准 - handler 中仍需捕获
pgerr.Code == "23505"或 MySQL 的ErrNo: 1062,转成 400 并提示“该账号已被注册” - validator 里的 DB 查询建议加
FOR SHARE(PostgreSQL)或SELECT ... LOCK IN SHARE MODE(MySQL),降低竞态概率,但不能根除
真正难的不是写那个 func(fl validator.FieldLevel) bool,而是让这个函数在高并发、可测试、可监控的前提下,稳定调通 DB 并返回有意义的结果。校验失败日志里如果只写“user_email_unique failed”,没人知道是 DB 挂了、网络抖动,还是真重复了。











