可扩展校验需解耦规则与执行:校验函数只接收原始数据并返回错误,业务规则封装为单一职责子函数;用Functional Options构造Validator注入上下文;避免struct tag校验器陷阱;路径/查询参数须手动安全转换与校验。
把校验规则硬编码在 handler 里,或者塞进一个 giant Validate() 函数,后续加个“支付金额 ≤ 用户余额”就得改函数签名、重写逻辑、补测试——这不是扩展,是推倒重来。真正可扩展的校验函数,得让规则和执行解耦。
正确做法:校验函数只接收原始数据(比如 map[string][]string 或已反序列化的结构体),返回 error 或 []string 错误消息列表;所有业务规则封装成独立函数,按需组合。
ValidateLogin(req *LoginReq) 是具体接口的入口,但内部不写 if/else,而是调用 validateRequiredFields(req)、validatePasswordStrength(req.Password)、validateUserExists(ctx, req.User)
validateEmailFormat 被注册、找回、邀请多个接口共用)很多校验依赖运行时信息:当前租户 ID、请求 IP、数据库连接、缓存客户端。如果把这些全塞进每个校验函数参数列表,调用点立刻爆炸。Functional Options 模式在这里不是用来构造 Client,而是构造 Validator 实例。
定义:type ValidatorOption func(*Validator),然后提供 WithDB(db *sql.DB)、WithTenantID(tenantID string)、WithContext(ctx context.Context) 等选项。
立即学习“go语言免费学习笔记(深入)”;
WithDB 里做 db.Ping()——构造阶段应轻量,校验时再检查连接有效性validateEmail,而非拆成两个 Optiongo-playground/validator 的 binding:"required,min=2" 很快就会撞墙。它不支持条件校验、跨字段约束、动态规则加载,更无法注入上下文。强行用它撑复杂场景,只会让错误信息变成 “Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag”,用户根本不知道哪里错了。
Address 内含 Province 和 City)若字段为 nil 指针,required 不触发——必须手动初始化或用 omitempty 配合额外判断Type == "email" 时,Value 必须符合邮箱格式” 这类逻辑,struct tag 表达不了,得靠代码分支func ValidateEmail(s string) bool 返回 false,框架不知道该归到哪个 field,最终报全局错误别信框架自动类型转换。chi 的 chi.URLParam(r, "id") 或 gin 的 c.Param("id") 返回 string,传 id=abc 就得到 "abc",strconv.Atoi("abc") panic 是迟早的事。
strconv.ParseUint(c.Param("id"), 10, 64),检查 error,再业务校验(如 id > 0)c.Query("page") 直接转 int,应先收进 url.Values,再映射到结构体(可用 mapstructure.Decode),最后走 validator 或自定义校验函数page="" 不等于未传——它是明确传了空值,应视为非法,而不是 fallback 到默认值 1validateRefundAmount 而不是 validateFloat64),错误消息要带字段名和业务含义(“退款金额不能超过订单实付金额”),否则再漂亮的模式也只是包装纸。