gorm的limit+offset分页必须校验参数,因gorm不清洗page=0、page=-1、pagesize=999999等非法值,直接拼sql易致查空、报错或拖垮db;无order排序结果不可预测;大offset性能断崖;count需独立实例否则返回错误总数。

为什么 GORM 的 Limit + Offset 必须加拦截校验
因为 GORM 不做任何分页参数清洗,page=0、page=-1、page_size=999999 会原样拼进 SQL,轻则查空,重则触发 MySQL 的 SQL ERROR 1235 或拖垮数据库。更隐蔽的问题是:没加 Order 的分页结果不可预测——MySQL/PostgreSQL 对无序查询不保证返回顺序,同一条 Limit(10).Offset(100) 多次执行可能返回不同记录。
-
Offset不是“第几页”的语义映射,而是数据库扫描并丢弃的物理行数;数据有插入/删除时,必然漏数据或重复 -
OFFSET 50000 LIMIT 20在百万级表上实际要扫描前 5 万行,响应时间从 20ms 拉到 1.5s+ -
Count(&total)若复用带Limit/Offset的链式 DB 实例,查出的total永远 ≤Limit值,这是 GORM v2 的设计行为,不是 bug
如何用 Scope 封装带校验的分页逻辑
比写重复的 Offset((p.Page-1)*p.Size) 更可靠的是用 Scope 统一收口:参数校验、排序强制、总数隔离、边界截断全在里面。它不依赖第三方插件,兼容所有 GORM v2 版本。
- 必须在
Scope内部做page = max(1, page)和size = clamp(size, 1, 100),不能指望前端传对 - 显式调用
db.Order("id ASC")或db.Order("created_at DESC, id DESC"),避免用status这类高重复字段排序 - 总数查询必须拆成独立 DB 实例:
query := db.Where(...),再分别调用query.Count()和query.Limit().Offset().Find() - 别在
Scope里调用Count——它会污染当前链,导致后续Find被限流
游标分页为什么不能只靠 WHERE id > ? 就完事
游标分页(WHERE id > ? ORDER BY id LIMIT 20)是大数据量唯一可选方案,但直接手写易出错:高并发下 created_at 毫秒级重复、复合排序字段索引未覆盖、游标值未做类型校验都会导致跳记录或死循环。
- 时间戳字段慎单独用于游标,必须补
id作为第二排序键:ORDER BY created_at DESC, id DESC - 游标值(如上一页最后的
id)必须校验非零且存在,否则WHERE id > 0可能扫全表 - 复合索引必须严格匹配排序字段顺序和方向,例如
INDEX(created_at, id)不支持ORDER BY created_at DESC, id DESC,得建INDEX(created_at DESC, id DESC) - 前端传来的游标值是字符串,需用
strconv.ParseInt显式转,失败就拒掉,不 fallback 到 offset 分页
分页拦截器最容易被忽略的三个点
真正上线后暴雷的往往不是语法错误,而是这几个隐性细节:Preload 关联分页失效、COUNT 查询慢于主查询、游标值跨服务未序列化。
- 在分页查询中用
Preload("Orders"),GORM 会先查出 10 个用户,再为每个用户发一条SELECT ... WHERE user_id IN (?),加载的订单远超 10 条,且无法控制“每个用户只取最新 3 个” - 当
WHERE条件含 JOIN 或子查询时,Count可能比主查询还慢;此时应缓存总数,或降级为估算值(如用TABLE_ROWS近似) - 游标值(如
last_id=12345)若经 Kafka 或 HTTP 跨服务传递,必须确保序列化/反序列化不丢失精度——int64转 JSON 时别被当成 float 科学计数法截断











