Prepare 防 SQL 注入的关键在于参数独立传输而非字符串拼接;若仍用 fmt.Sprintf 拼接 SQL 再传给 Prepare,则完全失效,且需按驱动规范使用占位符(MySQL 用 ?,PostgreSQL 用 $1),并及时 Close Stmt 防资源泄漏。

为什么 Prepare 能防 SQL 注入,但不是用了就安全
因为 Prepare 本身不处理参数拼接,它只是把 SQL 模板发给数据库预编译;真正起作用的是后续用 Exec 或 Query 传参时,驱动把值作为独立参数发送(而非字符串拼接)。如果仍用 fmt.Sprintf 拼接 SQL 再传给 Prepare,照样中招。
- 错误写法:
db.Prepare(fmt.Sprintf("SELECT * FROM users WHERE id = %d", userID))—— 这根本没走参数化,Prepare只是编译了已被污染的 SQL - 正确路径:SQL 字符串里用
?(MySQL/SQLite)或$1(PostgreSQL),参数单独传给Query/Exec - Go 的
database/sql驱动会自动把参数转成二进制协议数据,数据库引擎原生解析,绕过语法分析阶段,SQL 注入无从下手
MySQL 和 PostgreSQL 的占位符写法差异必须对齐驱动
写错占位符会导致 sql: expected 0 arguments, got 1 或直接 panic,不是所有驱动都兼容 ?。
- MySQL 驱动(如
go-sql-driver/mysql)只认?:"SELECT name FROM users WHERE age > ?" - PostgreSQL 驱动(如
lib/pq)只认$1,$2:"SELECT name FROM users WHERE age > $1" - SQLite 驱动(如
mattn/go-sqlite3)支持?和?NNN,但不支持$1 - 别在代码里硬编码占位符风格——连接哪个库,就按那个库的驱动规范写,混用等于白防
Prepare 后不显式 Close 会悄悄吃掉连接和内存
很多人以为 Stmt 是轻量对象,其实它背后绑定了一个数据库连接资源(尤其在连接池场景下),不关会泄漏。
-
Stmt不是线程安全的,不能跨 goroutine 复用;若需并发执行,要么每次Prepare+Close,要么用db.Query/db.Exec(它们内部会自动 prepare + close) - 高频调用场景建议复用
Stmt,但必须确保生命周期可控:defer stmt.Close()是基本操作 - 忘记
Close的典型现象:连接数缓慢上涨、too many connections报错、GC 压力变大(Stmt持有 C 层句柄)
哪些情况不能靠 Prepare 防注入,得换思路
表名、列名、ORDER BY 字段、LIMIT 数值(非 offset/count 参数)这些无法参数化,Prepare 对它们完全无效。
立即学习“go语言免费学习笔记(深入)”;
- 错误尝试:
"SELECT * FROM ? WHERE status = ?"→ 驱动报错,表名不能参数化 - 安全做法:白名单校验 + 显式映射,例如
map[string]bool{"users": true, "orders": true},不在其中就拒绝 - 动态排序字段同理:只允许
"created_at","updated_at"等固定字符串,用switch分支拼接,不接受任意输入 - 注意:哪怕用了
Prepare,如果业务层把用户输入塞进 SQL 字符串再执行,防线就从数据库退到了应用层,本质没防住
最易被忽略的一点:很多团队只在 CRUD 主流程用 Prepare,却忘了日志查询、后台导出、定时任务里的 SQL 同样要参数化——只要拼接了用户可控输入,风险就在。










