Go 1.22 的 database/sql 能防绝大多数 SQL 注入,前提是正确使用驱动匹配的占位符(MySQL 用 ?,PostgreSQL 用 $1)、杜绝字符串拼接、表名列名等结构部分严格白名单校验。

只要用对 Query 和 Exec 的占位符、不拼接字符串、不碰表名列名,Go 1.22 的 database/sql 就能防住绝大多数 SQL 注入——它不是“需要你配置才安全”,而是你一旦拼字符串,就立刻破防。
占位符必须严格匹配驱动,写错等于没防
Go 1.22 没改 database/sql 的接口设计,占位符仍由底层驱动决定,不能混用:
- MySQL 驱动(
github.com/go-sql-driver/mysql)只认?,写$1会报sql: expected 0 arguments, got 1或静默当字面量处理 - PostgreSQL 驱动(
github.com/lib/pq)只认$1、$2,写?直接报pq: syntax error at or near "?" - SQLite 驱动(
github.com/mattn/go-sqlite3)虽支持多种写法,但推荐统一用?,避免跨环境行为不一致 - 错误示例:
db.Query("SELECT * FROM users WHERE id = ? AND name = $1", 123, "alice")—— 多数驱动忽略第二个参数或 panic
表名、列名、ORDER BY 字段不能参数化,白名单是唯一解
? 和 $1 只能替换值(value),数据库在解析阶段就必须确定结构。试图参数化标识符会直接失败:
-
db.Query("SELECT * FROM ?", tableName)→ panic:sql: expected 0 arguments, got 1 -
db.Query("SELECT * FROM users ORDER BY ? DESC", sortCol)→ 多数驱动报语法错误或忽略 - 正确做法是预定义白名单:
validSort := map[string]bool{"created_at": true, "score": true, "name": true},查不到就拒掉 - 排序方向(
ASC/DESC)也要白名单校验,别信strings.ToUpper(dir)后直接拼 - 分表场景用
switch映射:case "prod": table = "users_prod",而不是"users_" + input
IN 子句和空切片必须手动展开,不能手拼字符串
用户传来的 []int64{1,2,3} 不能转成 "1,2,3" 塞进 SQL,否则等于开放注入入口:
- 错误:
idsStr := strings.Join(idsStrs, ","); db.Query("SELECT * FROM users WHERE id IN ("+ idsStr +")") - MySQL 驱动不支持
IN (?)自动展开多值,得生成对应数量的?:"IN (?, ?, ?)",再传三个参数 - 推荐用
sqlx.In(需引入github.com/jmoiron/sqlx):query, args, _ := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids),再db.Query(query, args...) - 空切片要单独处理:
if len(ids) == 0 { return errors.New("empty IDs") },否则IN ()语法错误
Prepare 后必须 Close,且别在循环里反复 Prepare
sql.Stmt 是有状态对象,持有连接资源,漏 Close 会导致连接池耗尽:
- 必须配对:
stmt, err := db.Prepare(...); defer stmt.Close(),尤其函数内多次调用时 - 别在 for 循环里反复
Prepare同一条语句——*sql.Stmt本身并发安全,应复用 - 如果语句只用一次,直接用
db.Query/db.Exec更简单,它们内部自动Prepare+Close - 错误现象:
sql: connection refused或查询长时间阻塞,往往就是Stmt泄漏
最易被忽略的点:动态 SQL 的边界在哪里。哪怕用了 ?,只要你在 WHERE 条件里拼字段名、操作符(如 "age " + op + " ?"),或者在 GORM.Raw() 里拼表名,防注入就彻底失效——参数化只保值,不保结构。

















