应定义为函数类型 type Specification[T any] func(T) bool,避免接口断言panic;组合用And/Or返回新Specification[T],支持短路与调试;依赖通过闭包注入,禁用全局变量。

Specification 接口怎么定义才不踩 runtime panic 坑
Go 没有泛型约束(1.18 之前)时硬套 Java 风格的 Specification<t></t>,运行时一调用 IsSatisfiedBy 就 panic:interface{} 无法直接断言。必须把类型检查前移到编译期,或至少让调用方明确承担类型责任。
推荐定义为函数类型,而不是结构体嵌接口:
type Specification[T any] func(T) bool
这样既支持链式组合,又避免空指针和类型断言失败。所有实现都得是闭包或具名函数,不能返回未初始化的 struct 实例。
- 不要用
type Spec interface { IsSatisfiedBy(interface{}) bool }—— 调用方每次都要手动类型转换,极易漏判 - 如果业务对象字段多、校验逻辑重,建议把
T设为指针类型(*Order),避免大对象拷贝 - 组合多个
Specification[Order]时,用And/Or辅助函数,别手写嵌套 if
And / Or 组合器为什么不能直接用 && ||
看似简单,但直接在 IsSatisfiedBy 里写 s1(obj) && s2(obj) 会丢失短路语义的控制权——你没法知道是哪个子规则失败,也没法在中间插入日志、指标或 fallback 行为。
立即学习“go语言免费学习笔记(深入)”;
真正可用的组合器必须返回新的 Specification[T],且保留各子项独立执行能力:
func And[T any](a, b Specification[T]) Specification[T] {
return func(t T) bool {
return a(t) && b(t) // 这里才是可控的短路
}
}
- 别在组合器里做
panic或log.Fatal—— 规则链要可预测,失败就该安静返回false - 如果某规则可能 error(比如查 DB),它就不该是
Specification[T],而应是func(T) (bool, error),走另一条错误处理路径 - 组合超过 3 层后,建议用切片 + 循环实现
All,比嵌套And(And(a,b),c)更易读和调试
如何让 Specification 支持依赖注入(比如 DB Client)
纯函数式 Specification[T] 天然不带状态,但真实业务规则常要查缓存、调下游、读配置。硬塞全局变量或单例,会导致单元测试无法隔离、并发不安全。
正确做法是把依赖提前闭包进去,生成具体规则实例:
func NewPriceInRangeSpec(min, max float64, db *sql.DB) Specification[*Order] {
return func(o *Order) bool {
price, _ := getProductPrice(o.ProductID, db) // 内部用传入的 db
return price >= min && price <= max
}
}
- 不要在
Specification函数体内 new DB 连接或初始化 client —— 生命周期失控,连接池爆掉 - 如果依赖项本身需要 context(如带 timeout 的 HTTP 调用),就把
context.Context作为参数加到闭包里,而不是塞进Specification类型定义 - 测试时直接传 mock db 或内存 map,不用改规则签名,也不用 interface{} 强转
什么时候不该用 Specification 模式
不是所有校验都适合抽成 Specification。当出现以下情况,强行套用反而增加理解成本和维护负担:
- 规则只用一次,且逻辑小于 3 行(比如
len(s) > 0)—— 直接写在 if 里更直观 - 规则之间强顺序依赖,比如“先查库存再扣减”,这不是“满足条件”,而是“执行动作”,该用 Command 模式
- 同一对象上叠加 5+ 个 Specification,且每个都要访问不同外部服务 —— 链式调用变成串行阻塞,性能瓶颈明显,该考虑并行化或预加载
最常被忽略的一点:Specification 只回答“是否符合”,不负责“不符合时怎么办”。错误提示、补偿操作、审计日志这些,得在调用层统一收口,别散落在各个 Specification 闭包里。










