go的database/sql默认自动预编译,只需正确使用占位符(mysql用?、postgresql用$1)并传入类型匹配的独立参数;拼接sql、混用占位符、动态标识符或类型不匹配会导致静默退化为字符串替换,引发注入风险。

Go 的 database/sql 默认就走预编译路径,只要写对占位符、传对参数,不用手动开开关;写错反而会静默退化成字符串拼接,埋下注入隐患。
用 db.Query 或 db.Exec 就够了,别一上来就 db.Prepare
绝大多数场景下,直接调用 db.Query("SELECT * FROM users WHERE id = ?", 123) 即可。底层驱动(如 go-sql-driver/mysql 或 lib/pq)收到带 ?(MySQL)或 $1(PostgreSQL)的 SQL 和独立参数后,会自动触发预编译流程——前提是连接未禁用、数据库支持、且参数类型合法。
手动 db.Prepare 只在明确需要复用同一语句多次时才必要,比如批量插入循环。否则:
• 每次都 Prepare + Close 反而增加开销
• 忘记 Close 会导致连接池资源泄漏
• 驱动通常按 SQL 字符串缓存预编译句柄,"WHERE id = ?" 和 "WHERE name = ?" 被视为两条不同语句
QueryRow 和 Query 的参数必须是变量或切片元素,不能是字符串拼接结果
错误写法会让预编译失效,且不报错:
-
db.Query("SELECT * FROM users WHERE name = '" + name + "'")→ 直接拼字符串,完全绕过预编译 -
db.Query("SELECT * FROM users WHERE id IN (?)", "1,2,3")→IN不支持单个占位符展开,多数驱动静默替换为字面量'1,2,3',查不到数据 -
db.Query("SELECT * FROM users WHERE id = ?", fmt.Sprintf("%d", id))→ 类型不匹配(传了string但字段是int),部分驱动退化为文本替换
正确做法:
• 值参数保持原生类型:db.Query("...", id, name, age)
• IN 动态长度需手动生成占位符:"WHERE id IN (, , )" 并传入对应数量的参数
• 所有参数必须是独立值,不能是表达式或拼接结果
表名、字段名、ORDER BY 不能用占位符,必须白名单校验后拼接
? 和 $1 只能用于**值(value)**,不能用于标识符(identifier)。以下全是错的:
-
db.Query("SELECT * FROM ?", tableName)→ 驱动报错或忽略,SQL 解析失败 -
db.Query("SELECT * FROM users ORDER BY ?", sortField)→ 多数驱动直接拒绝,返回语法错误 -
db.Query("SELECT ? FROM users", fieldName)→ 同样不被支持
可行方案只有:
• 提前定义允许的字段/表名白名单:validSortFields := map[string]bool{"name": true, "age": true}
• 校验输入是否在白名单内,再拼进 SQL:"ORDER BY " + sortField
• 拼接前做严格正则过滤(如 ^[a-zA-Z_][a-zA-Z0-9_]*$),禁止任何引号、分号、空格
PostgreSQL 和 MySQL 的占位符不能混用,驱动不兼容会静默失败
MySQL 驱动只认 ?,PostgreSQL 驱动(lib/pq)只认 $1、$2。混用不会报错,但会导致:
- MySQL 驱动看到
$1:当作普通字符串字面量处理,查询条件恒假 - PostgreSQL 驱动看到
?:无法识别占位符,可能报ERROR: syntax error at or near "?"或退化为文本替换
检查方式很简单:
• 看你 import 的驱动:_ "github.com/go-sql-driver/mysql" → 用 ?
• _ "github.com/lib/pq" → 用 $1、$2
• 切勿在同一个项目里混用两种驱动去连不同库(除非彻底隔离 db 实例)
最危险的是“看似运行正常但实际没走预编译”——比如类型不匹配、动态标识符拼接、占位符错位。这类问题不会炸出 panic,只会让 SQL 注入漏洞藏在生产环境里等你上线三个月后才发现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











