使用 database/sql 的 query 和 exec 时,必须用 ? 或 $1 等占位符传参,严禁字符串拼接;参数须为具体类型,禁用未断言的 interface{} 和 sql.rawbytes;动态 sql 需手动拼结构、占位符绑值;预编译 stmt 需谨慎使用;表名列名等不可参数化部分须白名单校验;权限最小化是防注入必要补充。

用 database/sql 的 Query 和 Exec 配合占位符
Go 原生 database/sql 包本身不拼接 SQL 字符串,只要你不手写字符串拼接,就天然避开大部分注入风险。关键在怎么传参——必须用 ?(MySQL/SQLite)或 $1(PostgreSQL)占位符,而不是 fmt.Sprintf 或 + 拼接。
常见错误现象:sql: no rows in result set 看似是查询失败,其实是参数没进占位符、被当字面量处理了;更危险的是,拼接后语句变成 WHERE name = 'admin' OR '1'='1' 这类逻辑绕过。
- MySQL/SQLite 用
?:db.Query("SELECT * FROM users WHERE id = ?", userID) - PostgreSQL 用
$1,$2:db.Query("SELECT * FROM users WHERE id = $1 AND status = $2", userID, status) - 绝对不要写:
db.Query("SELECT * FROM users WHERE id = " + strconv.Itoa(userID))或db.Query(fmt.Sprintf("... WHERE name = '%s'", name))
别把 sql.RawBytes 或 interface{} 当“万能参数”乱塞
占位符只接受具体类型值(int, string, time.Time 等),不能直接传 sql.RawBytes 或未断言的 interface{},否则驱动可能忽略类型校验,退化为字符串拼接。
使用场景:从 JSON 解析出的 map[string]interface{} 直接丢进 Exec 是高危操作;动态构建 WHERE 条件时,也容易误以为“反正用了 ? 就安全”,结果参数顺序错乱或类型不匹配。
- 传
interface{}前务必断言:v, ok := data["id"].(int); if !ok { /* error */ } -
sql.RawBytes是底层字节切片,不是可插占位符的值,需先转成string或[]byte - 动态 SQL(如可选条件)应手动拼接语句结构,但每个实际值仍走占位符:
query += " AND status = ?",再统一args = append(args, status)
ORM 如 gorm 或 sqlx 的坑:链式调用不等于自动防注入
gorm 默认用占位符,但一旦用到 Where("name = ?", name) 这种写法,它就安全;而 Where("name = '" + name + "'") 或 Where("name = ?", name).Where("age > " + ageStr) 后半段就崩了。
性能 / 兼容性影响:GORM 的 Scopes 或 Session 不改变参数绑定机制,但嵌套太多易掩盖拼接点;sqlx 的 NamedQuery 支持命名参数,但底层仍依赖驱动对 :name 的解析,PostgreSQL 驱动支持好,SQLite 驱动可能不认。
- GORM 中禁用
Raw和Session(&sql.Session{DryRun: true})外的字符串拼接 -
sqlx.NamedExec("INSERT INTO u (n) VALUES (:name)", map[string]interface{}{"name": n})安全,但确保 driver 支持命名参数(pgx行,sqlite3不行) - 任何 ORM 的
Find(&u, "id = ?", id)写法,和原生Query一样依赖你传对参数
预编译语句(Stmt)不是银弹,但能堵住某些边界漏洞
显式用 db.Prepare 创建 *sql.Stmt,再反复 Query/Exec,好处是语句只解析一次,且参数绑定更严格——比如传 nil 给非空字段会报错,而不是静默转成空字符串。
容易踩的坑:忘记 Close() 会导致连接泄漏;在短生命周期 HTTP handler 里频繁 Prepare 反而降低性能;更隐蔽的是,如果 Prepare 的 SQL 本身含拼接(比如用 fmt.Sprintf 构造表名),那预编译毫无意义。
- 适合场景:高频执行的固定语句,如登录验证、余额查询
- 表名、列名、ORDER BY 字段无法参数化,必须白名单校验或映射转换,例如:
if !validSortField(sortBy) { return errors.New("invalid sort field") } - 不用
defer stmt.Close()在 handler 里,改用stmt.Close()显式收尾,避免 defer 堆积
最常被忽略的一点:数据库权限最小化。即使代码全用占位符,如果应用账号有 DROP TABLE 权限,一条注入进 UNION SELECT 的语句仍可能拖库。防注入只是纵深防御的一环,不是终点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











