不用fmt.sprintf拼sql因会导致sql注入、空条件残留、参数顺序错乱;应使用预处理占位符(如$1、?)和结构化builder存储条件、参数、排序等片段,由业务层控制条件有效性,build时校验合法性并避免实例复用。

为什么不用 fmt.Sprintf 拼 SQL
直接用 fmt.Sprintf 拼接 SQL 看似简单,但会立刻掉进 SQL 注入、空条件残留、参数顺序错乱三个坑。比如 WHERE name = '%s' AND age > %d,当 name 为空时 WHERE 子句就残缺;更危险的是,用户输入的单引号能直接逃逸出字符串上下文。
- 所有用户可控字段必须走预处理参数(
$1,?),不能插进 SQL 字符串里 - WHERE 条件、ORDER BY 字段、JOIN 子句这些结构是“可选但需语法合法”的,不能靠字符串拼接临时补空格或
AND - PostgreSQL 用
$1占位,MySQL 用?,驱动本身不认fmt.Sprintf的结果,传给db.Query就 panic
Builder 结构体该存什么字段
一个可用的 SQL 建造者不是把整条 SQL 当字符串攒着,而是分层存结构化片段:条件列表、参数值切片、排序字段、分页偏移。这样既能按需追加,又能在最终生成时统一处理占位符顺序。
- 用
[]string存 WHERE 条件表达式(如"status = $1"),不是"status = ?"—— 占位符风格要和驱动对齐 - 用
[]interface{}存实际参数值,和条件表达式严格一一对应,避免索引错位 - 不要在结构体里存完整 SQL 字符串,否则每次
AddWhere都要重新strings.Join,性能差且难调试
示例关键字段:
type SQLBuilder struct {
whereClauses []string
params []interface{}
orderBy []string
limit, offset int
}
如何安全地添加动态 WHERE 条件
核心原则:条件判断逻辑必须在 AddWhere 之外做,建造者只负责“接收已判定有效的条件”。别让建造者自己判断 if name != "",那会污染职责,也容易漏判零值类型(比如 int 默认 0 是否算有效条件)。
立即学习“go语言免费学习笔记(深入)”;
- 业务层先聚合有效条件:
if req.Name != "" { builder.AddWhere("name = $1", req.Name) } -
AddWhere内部只做两件事:追加表达式字符串到whereClauses,追加参数值到params - 多个条件共用一个参数?不行。每个
AddWhere调用必须提供完整独立的表达式+参数组合,否则占位符索引会串
错误示范:builder.AddWhere("name = $1 AND status = $1", name) —— $1 被复用,但 params 只塞了一个值,驱动报 sql: expected 2 arguments, got 1
生成最终 SQL 和参数时的边界检查
调用 Build() 时不光要拼字符串,还得校验结构合法性。最常被忽略的是:没有 WHERE 条件时,不能留着孤零零的 WHERE 关键字;没有 ORDER BY 时,不能多出 ORDER BY。
- WHERE 部分用
strings.Join(builder.whereClauses, " AND ")拼,但前面得加if len(builder.whereClauses) > 0判断 - 参数切片直接传给
db.Query,但必须确保长度和 SQL 中占位符总数一致,否则驱动 panic - PostgreSQL 下
ORDER BY字段不能带$n,只能是列名或序号,所以builder.AddOrderBy("created_at DESC")是安全的,但builder.AddOrderBy("$1 DESC")会报错
真正容易被忽略的点:同一个 SQLBuilder 实例不能复用。因为 params 是切片,底层数组可能被多次 append 扩容,再调一次 Build() 会拿到上一次残留的参数。










