函数式验证优于struct tag+reflect,因其类型安全、可测试、编译期检查字段名,且支持上下文注入与错误累积;典型做法是用Validator切片组合多个纯函数验证器,统一收集并映射错误信息。

为什么不用 struct tag + reflect 做验证
因为 runtime 反射开销大、错误定位难、调试不直观,且无法在编译期捕获字段名拼写错误。函数式验证把规则显式写成闭包或纯函数,类型安全、可单元测试、支持提前 return,适合中等复杂度表单(比如登录、注册、配置提交)。
典型陷阱是把验证逻辑塞进一个大函数里,导致难以复用和组合。正确做法是每个校验项返回 error,靠短路逻辑串联:
func validateEmail(email string) error {
if email == "" {
return errors.New("email is required")
}
if !strings.Contains(email, "@") {
return errors.New("email format invalid")
}
return nil
}
如何组合多个验证函数并统一收集错误
单个函数只管一件事,但用户提交时需要一次性看到所有问题。不能用 || 或 return 早期退出,得累积错误。
- 定义
Validator类型为func() error,方便链式调用 - 用切片存多个
Validator,遍历执行,append 非 nil error 到[]error - 避免用 panic 或 log.Fatal —— 表单验证必须可控失败
示例组合:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
validators := []func() error{
func() error { return validateEmail(req.Email) },
func() error { return validatePassword(req.Password) },
func() error { return validateConfirmPassword(req.Password, req.ConfirmPassword) },
}
var errs []error
for _, v := range validators {
if err := v(); err != nil {
errs = append(errs, err)
}
}
怎么让验证器支持上下文(如数据库查重、依赖其他字段)
纯函数没法查 DB 或读 session,所以得把依赖“注入”进去。不要全局变量,也不要在 validator 闭包里捕获太多外部状态。
- 把依赖(如
*sql.DB、context.Context)作为参数传入 validator 工厂函数 - 工厂返回具体
func() error,闭包内只捕获必要变量 - 注意:DB 查询类验证应设 timeout,避免表单阻塞
例如邮箱唯一性检查:
func makeEmailUniquenessValidator(db *sql.DB, email string) func() error {
return func() error {
var exists bool
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", email).Scan(&exists)
if err != nil {
return fmt.Errorf("db query failed: %w", err)
}
if exists {
return errors.New("email already registered")
}
return nil
}
}
validator 怎么跟 HTTP handler 一起用才不乱
别在 handler 里硬编码一堆 if-else,也别把 validator 当 middleware —— 表单验证是业务逻辑,不是通用拦截。
- 在 handler 开头就构造 validator 切片,传入当前请求数据
- 执行后直接 switch
len(errs):0 就继续处理,>0 就return JSON 400带错误列表 - 错误信息别直接返回给前端(比如暴露 DB 字段名),用 map 映射成前端友好的 key(
"email"→"email_invalid")
关键点:validator 是无状态的,同一份代码可复用于 CLI、gRPC、HTTP,只要输入结构一致。
最容易被忽略的是错误信息的语义一致性 —— 同一个字段在不同场景下(注册/修改邮箱)可能有不同规则,但错误 key 应该统一,否则前端要写多套提示逻辑。

















