必须用参数化查询防止SQL注入,值用占位符(?或$1),表名列名等结构部分需白名单校验,Scan需严格匹配字段顺序和类型,ORM原生SQL仍需手动防护。

用 database/sql 的 Query 和 Exec 时必须传参,不能拼字符串
Go 的 database/sql 本身不支持字符串插值式查询,这是好事——但很多人误以为只要用了 fmt.Sprintf 拼出 SQL 再传给 Query 就“安全了”,其实完全不是。SQL 注入就藏在这种看似无害的拼接里。
正确做法是把用户输入作为参数传入,由驱动(如 mysql 或 pq)负责转义和绑定。数据库看到的是预编译语句 + 独立参数,根本不会把参数当 SQL 解析。
- ❌ 错误:
db.Query("SELECT * FROM users WHERE name = '" + name + "'") - ✅ 正确:
db.Query("SELECT * FROM users WHERE name = ?", name)(MySQL)或db.Query("SELECT * FROM users WHERE name = $1", name)(PostgreSQL) - ⚠️ 注意:占位符语法取决于驱动,
?是 MySQL/SQLite,$1,$2是 PostgreSQL,不能混用
批量插入或动态字段名时,不能参数化,得靠白名单校验
参数化只适用于**值(value)**,对表名、列名、ORDER BY 字段、LIMIT 数值等**结构部分**无效。试图用 ? 替换表名会直接报错:sql: expected 0 arguments, got 1。
这时候必须提前约束输入范围,用白名单而非黑名单过滤。别信“只要去掉单引号就安全”这种想法——SQL 注入不止靠单引号。
立即学习“go语言免费学习笔记(深入)”;
- ❌ 危险:
db.Query("SELECT * FROM " + tableName + " WHERE id = ?", id) - ✅ 安全:先检查
tableName是否在[]string{"users", "orders", "products"}中,不在就拒绝 - ? 动态排序字段同理:只允许
sortField是"created_at"或"name",然后拼进 SQL;不要尝试用参数传字段名
用 Scan 接收结果时,变量顺序和类型必须严格匹配 SELECT 字段
这不是注入问题,但常因疏忽导致运行时 panic 或静默数据错位,尤其在多人协作改表后容易漏掉同步代码。
Scan 不做字段名映射,只按 SELECT 后的字段**位置**和**类型**绑定。一旦 SQL 改了顺序,或加了新字段没更新 Scan 参数,轻则值错乱,重则 sql: Scan error on column index X。
- ✅ 推荐写法:显式列出字段,避免
SELECT *——SELECT id, name, email FROM users - ✅
Scan时用具名变量或结构体指针,别用匿名变量堆砌:err := row.Scan(&u.ID, &u.Name, &u.Email) - ⚠️ 注意:NULL 值要用
sql.NullString等类型接收,普通string遇到 NULL 会报错
ORM 如 GORM 默认开启参数化,但自定义原生 SQL 仍需手动防护
GORM 这类 ORM 对 Create、Find、Where 等方法内部做了参数化封装,确实省心。但它也提供 Raw 和 Exec 执行原生 SQL——这时所有防护归零,完全交还给你。
常见坑是:以为用了 GORM 就高枕无忧,结果在 db.Raw("UPDATE users SET status = ? WHERE id IN (?)", status, ids) 里,ids 是切片,而 GORM 不自动展开切片为多个问号,直接传进去会变成 WHERE id IN (?)",查不到数据还难排查。
- ✅ 正确展开切片:用
sqlx.In(配合sqlx.Rebind)或手动生成占位符串,再db.Raw(sql, args...) - ⚠️ 切记:GORM 的
Where("name = ?", name)是安全的,但Where("name = " + name)是致命的 - ? 线上环境建议开启 GORM 的
Logger,看实际执行的 SQL 是否含未转义内容
参数化不是开关,是贯穿 SQL 构造每一环的习惯。最危险的不是不会写,而是某一行忘了——尤其是动态 SQL 和 ORM 混用时。










