Go中SQL注入防护的核心是始终使用占位符参数化查询,禁用字符串拼接;表名列名等动态部分须白名单校验;ORM的Raw方法需显式用占位符,输入过滤不能替代参数化。

用 database/sql 的 Query 和 Exec 配合占位符
Go 标准库的 database/sql 本身不拼接 SQL 字符串,只要不用 fmt.Sprintf 或字符串拼接构造查询语句,就能避开绝大多数注入风险。关键在于始终使用问号占位符(?)或命名参数(如 $1、:name,取决于驱动),由底层驱动做参数绑定。
例如 PostgreSQL 驱动(lib/pq)用 $1,MySQL 驱动(go-sql-driver/mysql)用 ?:
rows, err := db.Query("SELECT name FROM users WHERE id = $1 AND status = $2", userID, "active")如果硬写成:
db.Query("SELECT name FROM users WHERE id = " + strconv.Itoa(userID))——这就直接把变量塞进 SQL 字符串里,完全失去参数化保护。
- 所有用户输入(URL 参数、表单字段、Header 值)必须走占位符传参,不能拼进 SQL 字符串
- 表名、列名、ORDER BY 字段等无法参数化的部分,只能白名单校验或映射转换,不能靠“转义”糊弄
- 注意:
sql.Named是命名参数的封装,本质仍是绑定,不是字符串替换
避免用 fmt.Sprintf 拼接 SQL 查询条件
常见错误是动态构建 WHERE 子句时,把用户输入直接插进字符串:
立即学习“go语言免费学习笔记(深入)”;
query := fmt.Sprintf("SELECT * FROM products WHERE category = '%s'", r.URL.Query().Get("cat"))攻击者传入 cat=electronics' OR '1'='1 就能绕过条件。这种写法在 Go 里没有语法错误,但等于放弃全部防护能力。
- 条件逻辑应由代码分支控制,而不是字符串拼接。比如用
if判断是否添加AND price > ? - 若需动态列筛选,提前定义合法字段列表:
validCols := map[string]bool{"name": true, "price": true},再查表 - 像
ORDER BY这类语句,只接受预设值:if order == "price" { query += " ORDER BY price" }
警惕 ORM 中的原始 SQL 和 Raw 方法
GORM、SQLx 等库提供 Raw 或 Session.Raw 方法执行原生 SQL。这些方法**不会自动参数化**传入的第二个及后续参数,除非显式使用占位符:
db.Raw("SELECT * FROM users WHERE email = ?", email).Scan(&user)但下面这个就危险:
db.Raw(fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)).Scan(&user)——和手写 fmt.Sprintf 一样失效。
-
Raw的第一个参数是 SQL 模板,后面才是参数;它不解析 SQL 字符串里的引号或变量 - GORM 的
Where("email = ?", email)是安全的,但Where("email = '" + email + "'")不是 - 用
Scopes或链式Where替代拼接,比依赖Raw更可控
别信“输入过滤”或“单引号转义”能防注入
有人给字符串加 strings.ReplaceAll(input, "'", "''") 或用正则删掉分号,这是典型误区。SQL 注入不只靠单引号,还可用十六进制、Unicode 编码、注释符(/* */)、函数嵌套等方式绕过。PostgreSQL 支持 CHR(39) 构造单引号,MySQL 支持 0x27,这些都逃得过简单替换。
- Go 没有类似 PHP 的
mysql_real_escape_string,也不该有——因为参数化才是唯一正解 - 对输入做长度限制、类型断言(
strconv.Atoi)、正则白名单(如邮箱格式)是合理的,但它们是辅助校验,不是注入防护主力 - 真正难处理的是动态排序、动态表名、动态列——这些问题的答案从来不是“怎么转义”,而是“能不能换种设计绕过去”
参数绑定这件事,在 Go 里只要不主动破坏,基本是默认安全的。真正容易出问题的地方,永远是开发者以为“我只拼了一小段 SQL,应该没事”,结果那一小段就成了整个防线的突破口。










