go 的 database/sql 不支持切片直接展开为 in 参数,需手动转 interface{}、生成等长 ? 占位符,空数组必须退化为 where 1=0,且不同数据库参数上限差异大,须按目标库分批处理。

Go 中 database/sql 无法直接展开切片,必须手动构造占位符
Go 的标准库 database/sql 驱动不支持把 []int 或 []string 直接传给单个 ? 占位符——它会把整个切片当做一个参数值,导致 SQL 实际执行的是 WHERE id IN ('[1 2 3]'),而非 WHERE id IN (1, 2, 3)。
正确做法是:先将原始切片转成 []interface{},再用 strings.Repeat 或循环生成等长的 ? 占位符串,最后拼进 SQL 并展开参数。
-
ids := []int{1, 5, 9}→ 转为[]interface{},不能跳过这步 - 占位符生成推荐写法:
strings.Repeat("?,", len(ids)-1) + "?",比map更安全(避免误读用户数据) - 最终 SQL 是
"SELECT * FROM users WHERE id IN (?, ?, ?)",不是"IN (?)"
空数组不处理,IN () 在所有主流数据库都报语法错误
这是线上最常踩的坑:前端传了个空数组 [],后端没拦截就直接走 IN 查询,MySQL 报 You have an error in your SQL syntax,PostgreSQL 报 syntax error at or near ")",SQLite 直接 panic。
必须显式判断长度并退化逻辑:
- 长度为 0 时,改用
WHERE 1=0(返回空结果集,语义明确) - 不要依赖 ORM 自动 fallback——Django 会抛
EmptyResultSet,Knex 静默返回空 slice,Drizzle 在 SQLite 下对 >999 项直接throw - 若业务需区分“无匹配”和“条件为空”,得在上层加标记,不能靠 SQL 返回结果推断
PostgreSQL 和 MySQL 对 IN 参数数量限制差异极大
同一套动态占位符逻辑,在不同数据库可能表现完全不同:
- PostgreSQL 默认单语句最多
65535个参数,超限报too many parameters - MySQL 受
max_allowed_packet和预编译缓存影响,实际阈值常卡在 1–2 万左右 - SQLite 在 Drizzle 下硬限制
999项,超过就panic: too many SQL variables - 跨库项目别幻想“一次写完到处跑”,必须按目标 DB 设置分批阈值(如 MySQL 用 500,SQLite 用 999)
ORM 的 whereIn() 不是银弹,边界逻辑仍要自己兜底
Prisma 的 where: { id: { in: ids } }、SQLAlchemy 的 filter(Model.id.in_(ids)) 看似省心,但它们只解决“参数绑定”问题,不处理:
- 空数组:Prisma 返回空数组,但不会自动改写成
WHERE 1=0;SQLAlchemy 抛EmptyResultSet需显式捕获 - 超长列表:Drizzle 的
inArray()在 SQLite 下直接崩溃,Knex 的whereIn()不报错但查不到数据(因参数被截断) - 类型混用:传
["1", "2"]给整型字段,MySQL 可能隐式转换但放弃索引,PostgreSQL 直接报operator does not exist: integer = text
真正安全的做法,永远是:拿到原始输入后,先校验类型与长度,再决定走单次查询还是分批,最后才交到 ORM 或原生驱动手里——这个判断层不能省。











