必须使用参数化查询而非字符串拼接构造SQL,因任何客户端过滤均无法彻底防御SQL注入;Go中应通过db.Query等接口传参,由驱动分离SQL结构与数据,确保用户输入仅作值传递。

为什么不能用 fmt.Sprintf 拼接 SQL 查询
直接拼接字符串构造 SQL 语句,哪怕加了 strings.Replace 或正则过滤,也拦不住绕过型注入。比如用户输入 admin' -- ,拼成 WHERE username = 'admin' -- ',注释掉后续校验逻辑;更隐蔽的是 Unicode 变体、宽字节截断、多语句分隔符(;)等,根本没法靠“清洗”兜底。
Go 的 database/sql 原生支持参数化查询,底层通过驱动将参数与 SQL 语句分离传输,数据库服务端在解析阶段就已确定执行计划,变量值仅作为纯数据传入,不参与语法解析 —— 这才是防注入的唯一可靠路径。
必须用 db.Query / db.Exec 的占位符接口,别碰 db.QueryRow 的字符串拼接变体
常见错误是误以为 db.QueryRow("SELECT * FROM users WHERE id = " + strconv.Itoa(id)) 安全,其实只要拼接进 SQL 字符串,就等于放弃防护。正确做法只有一种:把参数独立传入,由驱动处理绑定。
- MySQL 驱动(如
github.com/go-sql-driver/mysql)用?占位:db.Query("SELECT name FROM users WHERE status = ? AND age > ?", "active", 18) - PostgreSQL 驱动(如
github.com/lib/pq)用$1,$2:db.Query("SELECT name FROM users WHERE status = $1 AND age > $2", "active", 18) - SQLite 驱动(如
github.com/mattn/go-sqlite3)支持?和命名参数:name,但推荐统一用?保持可移植性
注意:占位符只能用于**值(value)位置**,不能用于表名、字段名、ORDER BY 子句或 LIMIT 数值——这些属于 SQL 结构,必须通过白名单校验后硬编码或用 switch 分支控制。
立即学习“go语言免费学习笔记(深入)”;
动态 WHERE 条件时,别手写 WHERE 1=1 拼接,用切片构建参数化查询
当搜索条件不确定(比如用户可选按 name、email、status 过滤),容易陷入“先拼 SQL 再补问号”的陷阱,导致参数顺序错乱或漏绑定。正确做法是用 []interface{} 收集参数,用 strings.Builder 动态拼接 WHERE 子句(仅结构部分),最后统一分发。
// 示例:构建带条件的查询
var whereParts []string
var args []interface{}
<p>if name != "" {
whereParts = append(whereParts, "name LIKE ?")
args = append(args, "%"+name+"%")
}
if status != "" {
whereParts = append(whereParts, "status = ?")
args = append(args, status)
}</p><p>query := "SELECT * FROM users"
if len(whereParts) > 0 {
query += " WHERE " + strings.Join(whereParts, " AND ")
}</p><p>rows, err := db.Query(query, args...)
关键点:所有 ? 占位符都来自 args 切片,顺序严格对应;SQL 字符串里不出现任何用户输入内容。
警惕 ORM 库的“自动转义”幻觉,检查生成 SQL 是否真用了参数化
像 gorm 或 sqlx 看似简化了操作,但若调用 Where("name = '" + input + "'") 或使用 Find(&u, "name = ?", input) 以外的非常规写法,仍可能触发字符串插值。尤其注意:
-
gorm.DB.Where("name = ?", name).Find(&user)✅ 安全 -
gorm.DB.Where("name = '" + name + "'").Find(&user)❌ 危险 -
sqlx.Select(&users, "SELECT * FROM users WHERE "+condition, args...)—— condition 若含用户输入,即失效
上线前建议开启数据库日志(如 MySQL 的 general_log)或用 sqlmock 测试,确认最终执行的 SQL 中没有用户数据明文出现。
最易被忽略的是批量操作和 IN 子句:驱动通常不支持 IN (?, ?, ?) 动态长度绑定,需手动展开参数或改用临时表;这地方一偷懒,就退回拼接老路。


















