specification模式在go中应聚焦业务语义组合复用,避免泛化为通用orm查询器;推荐函数式接口toquery() (string, []interface{}),用结构体实现具体规格,通过and/or函数组合,dao层安全接入并校验空sql。

Go 里做查询条件抽象,Specification 模式不是银弹,但能避免 if-else 泛滥和重复拼接 WHERE 逻辑 —— 关键在于别把它做成通用 ORM 查询构建器,而是聚焦「业务语义」的组合与复用。
为什么直接写 map[string]interface{} 或结构体字段容易出问题
常见做法是定义一个 SearchInput 结构体,每个字段对应一个可选查询条件,再在 DAO 层用一堆 if 判断是否非零值来拼 SQL。这看似简单,但很快会失控:
- 字段语义模糊:比如
Status是 int 还是 string?CreatedAtGt和CreatedAtAfter命名不统一 - 组合逻辑散落各处:用户列表要「已激活 + 非测试账号 + 最近7天注册」,订单列表要「已支付 + 金额 >100 + 非退款单」,每处都重写判断
- 无法静态校验:传入
Status: 999可能只在运行时才报错,SQL 执行失败才暴露 - 测试困难:每个组合都要写独立 case,覆盖率难保障
Specification 接口该长什么样(Go 实现要点)
Go 没有泛型约束前的 Specification 很容易写成空接口或反射驱动,反而更难维护。推荐用函数式 + 接口组合,核心就两条:
- 定义
type Specification interface { ToQuery() (string, []interface{}) }—— 返回 SQL 片段和参数,不碰 DB 实例 - 每个具体规格封装成结构体(如
ActiveUserSpec),实现ToQuery;组合用And/Or函数返回新Specification - 避免继承/泛型嵌套:不要搞
Specification[T],Go 的类型系统更适合「组合行为」而非「泛化数据」
示例:
type ActiveUserSpec struct{}
<p>func (s ActiveUserSpec) ToQuery() (string, []interface{}) {
return "status = ? AND is_test = ?", []interface{}{1, false}
}</p><p>func And(a, b Specification) Specification {
return andSpec{a: a, b: b}
}</p><p>type andSpec struct {
a, b Specification
}</p><p>func (s andSpec) ToQuery() (string, []interface{}) {
sqlA, argsA := s.a.ToQuery()
sqlB, argsB := s.b.ToQuery()
return "(" + sqlA + ") AND (" + sqlB + ")", append(argsA, argsB...)
}</p>
DAO 层怎么安全接入 Specification
DAO 不该知道 Specification 内部怎么拼 SQL,只负责接收、调用、注入参数。重点有三:
- 必须校验返回的 SQL 片段是否为空:
if sql == "" { return "", nil },避免生成WHERE ()这类非法语句 - 参数必须用
...interface{}展开,不能直接传 slice —— 否则db.Query(sql, args)会把整个 slice 当第一个参数 - 建议加一层包装函数,统一处理分页、排序等固定逻辑,让 Specification 只管「过滤」:
func (r *UserRepo) FindBy(spec Specification, limit, offset int) ([]User, error) { where, args := spec.ToQuery() if where == "" { where, args = "1=1", []interface{}{} } sql := "SELECT * FROM users WHERE " + where + " ORDER BY id DESC LIMIT ? OFFSET ?" args = append(args, limit, offset) // ... 执行查询 }
哪些场景不该硬套 Specification
这个模式容易被滥用成“所有查询都得走 Specification”,结果反而增加理解成本:
- 单字段精确查询(如
FindByID(id int))—— 直接写方法更清晰,没必要包装成IDEqualSpec - 涉及 JOIN 或子查询的复杂条件(如「查有未完成订单的用户」)—— Specification 通常只负责
WHERE主表字段,JOIN 逻辑应放在 DAO 方法里显式控制 - 动态字段排序(
ORDER BY ?)—— SQL 注入风险高,Specification不适合处理这类元信息 - 需要缓存键生成的场景(如 Redis key)——
ToQuery()返回的是 SQL,不是稳定字符串,没法直接当 cache key
真正值得抽象的,是那些「反复出现、有明确业务含义、且组合频繁」的条件块,比如 PaidOrderSpec、NonExpiredTokenSpec、AdminOrOwnerSpec —— 它们的名字本身就在表达领域逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











