mysql驱动只认?,postgresql只认$1、$2,写错占位符会导致预编译静默失效,参数被当作字面量拼接,引发sql注入风险。

占位符写错会导致预编译静默失效
MySQL 驱动只认 ?,PostgreSQL 只认 $1、$2,SQLite 两者都支持但行为不一致。写错占位符不会报错,而是把参数当字面量传进去——等价于字符串拼接。
常见错误现象:sql: expected 0 arguments, got 1(PostgreSQL 下用 ?)、查询永远无结果(MySQL 下误写 $1)、或更危险的“看似成功实则未绑定”。
- MySQL 场景下必须用
?:db.Query("SELECT * FROM users WHERE id = ?", id) - PostgreSQL 场景下必须用
$1:db.Query("SELECT * FROM users WHERE id = $1 AND status = $2", id, status) - 跨数据库项目建议封装统一的
QueryBuilder,或直接用 GORM 这类屏蔽差异的 ORM
动态 SQL 中值与结构必须严格分离
占位符只能出现在值的位置(WHERE 右侧、IN 列表、函数参数),不能用于表名、字段名、ORDER BY、GROUP BY 或子句关键字。试图写 ORDER BY ? 会直接 panic:sql: expected 0 arguments, got 1。
真正翻车的地方不是“没用占位符”,而是“以为用了占位符就安全”,结果在 ORDER BY 或表名上拼了用户输入。
- 排序字段必须白名单校验:
validSortFields := map[string]bool{"name": true, "created_at": true, "score": true} - 排序方向也得白名单:
if sortDir != "ASC" && sortDir != "DESC" { return err } - 分表场景下,表名映射必须显式:
switch tenantID { case "prod": table = "users_prod" },而非"users_" + tenantID
IN 子句和可变参数切片要手动对齐
WHERE id IN (?) 是无效写法,所有驱动都不支持单个占位符展开为多个值。多数情况下会静默替换为字面量 '1,2,3',查不到数据;更糟的是某些驱动可能把它当字符串匹配,埋下逻辑漏洞。
正确做法是动态生成占位符串,并确保参数数量与之严格对应。
- 先构造占位符:
placeholders := make([]string, len(ids)),然后填for i := range ids { placeholders[i] = fmt.Sprintf("$%d", i+1) } - 拼成
WHERE id IN (" + strings.Join(placeholders, ", ") + ")" - 参数必须是
[]interface{}类型切片,且元素顺序、类型一一匹配:args := make([]interface{}, len(ids)),再逐个赋值 - 别传
interface{}未断言的值,例如从map[string]interface{}直接取data["id"]就塞进去——必须先断言:v, ok := data["id"].(int); if !ok { /* error */ }
预处理语句复用时要注意生命周期和并发安全
db.Prepare 不是“越早调用越安全”,它适合高频固定结构查询(比如每秒上千次的用户 ID 查询)。普通场景下用 db.Query 更直白,且驱动通常自动缓存执行计划,性能差异极小。
硬套 Prepare 反而容易出问题:忘记 Close() 会导致连接池资源泄漏;在 HTTP handler 里反复调用会快速耗尽 MySQL 的 max_prepared_stmt_count(默认 16K);事务内创建的 *sql.Stmt 无法跨事务复用。
- 高频语句应在初始化阶段创建并保存为包级变量或结构体字段:
var stmtGetUser *sql.Stmt - 必须调用
stmt.Close(),Go 1.19+ 有警告但不 panic -
*sql.Stmt不是并发安全的,不能跨 goroutine 复用 - 扫描可能为 NULL 的字段时,必须用
sql.NullString、sql.NullInt64等类型,否则Scan遇到nil会 panic:sql: Scan error on column index 0: unsupported Scan, storing driver.Value type <nil> into type *string</nil>
复杂点不在占位符本身,而在你如何把用户输入分进“能参数化的值”和“必须白名单校验的结构”这两类——分错一个字符,整个防护就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











