db.prepare 不是银弹,须避免循环调用、动态标识符需白名单校验、参数须严格匹配占位符、事务内stmt不可跨事务复用、仅参数位置防注入,拼接sql仍危险。

db.Prepare 是 Go 原生操作数据库时启用预处理语句的唯一入口,但它不是“用了就安全、就快”的银弹。是否启用、何时复用、怎么传参,直接决定你能否真正规避 SQL 注入,以及是否意外拖慢性能。
为什么 db.Prepare 不能在循环里反复调用
每次调用 db.Prepare 都会向数据库发送一次 SQL 模板、触发一次编译、生成一个独立的执行计划,并占用一个服务端预处理句柄。MySQL 默认限制每个连接最多 16K 个预处理语句(max_prepared_stmt_count),反复 Prepare 很快就会报错:ERROR 1461: Can't create more than max_prepared_stmt_count statements (current value: 16382)。
实操建议:
- 预处理语句应在初始化阶段或函数外提前创建,保存为包级变量或结构体字段,例如
var stmtGetUser *sql.Stmt - 若需动态表名或列名(如分表查询),
db.Prepare无法处理——占位符?或$1只能代入值,不能代入标识符;此时必须手动校验并拼接,但要严格白名单过滤 - 避免在 HTTP handler 内直接
db.Prepare,除非明确控制生命周期(如用sync.Once)
Stmt.QueryRow 和 Stmt.Query 的参数传递规则
预处理语句执行时,参数类型和顺序必须与 SQL 中占位符完全匹配。Go 的 database/sql 不做隐式转换,比如把 int64 传给 WHERE id = ?(对应 MySQL INT)通常没问题,但传 nil 给非 NULL 列会触发驱动层错误,而传字符串给数字列可能静默失败或报 sql.ErrNoRows。
常见错误现象:
-
sql: converting argument $1 type: unsupported type nil, a nil pointer:传了nil但没用sql.NullString等包装类型 - 扫描失败但无 panic:例如
Scan(&id)时id是int,但数据库返回的是NULL,应改用sql.NullInt64 - PostgreSQL 报
pq: got 1 parameters but the statement requires 0:SQL 里没写$1却传了参数,多见于 Copy-Paste 错误
正确做法是始终按 SQL 占位符数量和顺序传参,且对可能为 NULL 的字段使用对应 sql.Null* 类型。
事务内使用预处理语句的两个硬约束
在事务中调用 db.Prepare 会返回一个绑定到该事务的 *sql.Stmt,它只能在该事务上下文中执行。一旦事务提交或回滚,这个 Stmt 就失效,后续调用会报 sql: statement is closed 或数据库层错误。
这意味着:
- 不要把事务内
tx.Prepare的结果缓存起来跨事务复用——它跟事务生命周期强绑定 - 如果想复用预处理逻辑,应在事务外用
db.Prepare创建 Stmt,再在tx.Stmt(stmt)方法中获取事务专属副本(MySQL 驱动支持,PostgreSQL 驱动需确认版本) - 显式关闭事务内 Stmt 并无必要,但必须确保事务对象本身被
Commit()或Rollback(),否则连接不会释放
预处理是否真能防 SQL 注入?关键在「不拼接」
预处理语句的安全性不来自函数名,而来自协议层隔离:命令(SQL 模板)和数据(参数)走不同信道,数据库服务端绝不会把参数当 SQL 解析。但如果你写了这样的代码:
query := "SELECT * FROM " + tableName + " WHERE id = ?" stmt, _ := db.Prepare(query)
那 tableName 仍可被注入——因为表名根本没进预处理流程。只有 ? 或 $1 位置的值才受保护。
所以真正防注入的边界非常窄:
- ✅ 用户输入只能出现在
Stmt.Exec/Query的参数列表里 - ❌ 用户输入绝不能参与 SQL 字符串拼接,哪怕只拼一个表名、一个 ORDER BY 字段
- ⚠️ 驱动行为有差异:MySQL 使用
?,PostgreSQL 使用$1,SQLite 兼容两者,但混合写法(如WHERE id = ? AND name = $1)会导致驱动解析失败
最易被忽略的一点:日志里打印带参数的 SQL 看似方便调试,但若直接拼接参数值到字符串,等于亲手绕过所有防护。应该分别记录 SQL 模板和参数数组,而不是合成后的“完整 SQL”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











