gorm分页必须用limit和offset配合确定性order,禁用无序分页;参数需校验防panic;总数查询须独立执行;大数据量应改用游标分页。

Limit 和 Offset 是 GORM 分页唯一可靠的基础,Order 不是可选项而是强制前提——没加排序的分页结果随时会漏数据或重复。
分页必须显式写 Order,否则查到什么全看数据库心情
MySQL、PostgreSQL 对无 ORDER BY 的查询不保证行序,哪怕表只读、没并发写入,同一条 Limit/Offset 查询多次执行也可能返回不同记录。这不是 GORM 的 bug,是 SQL 标准行为。
常见错误现象:db.Limit(10).Offset(100).Find(&users) 返回空 slice,但 err == nil;翻页时某条记录突然消失或重复出现。
- 必须用
Order("id ASC")或Order("created_at DESC, id DESC")这类带确定性顺序的组合 - 避免单用
Order("status ASC")—— status 值重复太多,数据库内部排序不稳定 - 时间字段慎用
Order("created_at DESC"):高并发下可能有毫秒级重复,补上, id DESC更稳妥 -
Order("RAND()")不能用于分页,性能差且无法支持下一页续查
Offset 参数别直接算,先校验再转换
Offset((page - 1) * pageSize) 看似标准,但 page=0、page=-1、pageSize=abc 都会让 GORM panic 或生成非法 SQL。GORM 不做任何参数清洗,前端传啥它就拼啥。
实操建议:
- 用
strconv.Atoi解析前先判空,非数字直接返回400 Bad Request - page 小于 1 时强制设为 1,不接受 “第 0 页” 这种语义
- pageSize 必须限制范围(如 1–100),超限要么截断(
min(p.Size, 100)),要么拒掉 - 别信
c.ShouldBindQuery(&p)自动容错——它对字符串转 int 失败时返回 error,得显式处理,不能忽略
总数查询必须独立执行,别和分页共用一个 db 实例
db.Where("status = ?", "active").Limit(10).Offset(20).Count(&total) 查出来的 total 永远 ≤10,因为 Count 在 GORM v2 中会复用前面链上的 Limit/Offset,这不是 bug,是设计如此。
正确做法是拆成两个独立查询:
- 先用
db.Where(...)构建条件 db 实例,存为变量(比如query := db.Where(...)) - 再分别调用
query.Count(&total)和query.Limit(...).Offset(...).Find(&list) - 如果 WHERE 条件复杂(含 JOIN、子查询),
Count可能比主查询还慢,考虑缓存或降级为估算值 - 别用
Find(&list).RowsAffected取总数——它返回的是当前页条数,不是全量
大数据量时 Offset 必须放弃,游标分页不是备选而是首选
当表行数超 10 万,Offset(50000) 在 MySQL 上实际要扫描并丢弃前 5 万行,响应时间从 20ms 拉到 1.5s+。这不是 GORM 能优化的,是 SQL 分页模型的硬伤。
游标分页绕过 Offset 的核心是:用上一页最后一条记录的排序字段值做条件。
- 首次请求:
db.Order("id ASC").Limit(20).Find(&users) - 后续请求传
last_id(不是页码),查:db.Where("id > ?", lastID).Order("id ASC").Limit(20).Find(&users) - 排序字段必须有索引,且值全局单调(
id最稳,created_at需搭配id防重复 - 前端需传递
last_id,后端不做额外校验(如是否真实存在),避免空结果陷阱
游标分页没法跳转任意页,但换来的是稳定性和线性性能——这才是高并发、大数据场景下的真实约束。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











