gorm分页必须用独立db实例执行count和find,page scope须置最后,order为必需且需索引,多条件组合需严格顺序,preload需拆查id再关联,page字段全指针并截断,游标分页更优。

直接用 Limit 和 Offset 拼分页,别信“自动分页封装”——GORM 本身没有 Paginate 方法,所有所谓一键分页都是自己或第三方写的,容易在多条件组合时污染链式状态、漏数据、panic。
为什么不能复用同一个 *gorm.DB 实例做 Count + Find
GORM 的链式调用(如 Where、Joins、Preload)会修改原 *gorm.DB 实例的内部状态。当你在一个 Scope 里先调 db.Count(&total),它会把 LIMIT 和 OFFSET 也带进后续 Find,导致查不到数据或 panic;反过来,如果先 Find 再 Count,Count 可能返回的是加了 LIMIT 后的条数(永远 ≤ pageSize)。
- 常见错误现象:
db.Scopes(StatusScope, DateRangeScope, Page(p)).Find(&list)返回空,但去掉Page(p)就有数据 - 正确做法:在分页 Scope 内部新建干净实例,例如
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total) - 更稳妥写法:不依赖当前 db 链,显式用
db.Model(&User{}).Where(...).Count(&total),Where 条件需和主查询完全一致
多条件组合时 Scopes 的顺序必须严格控制
多个 Scope(比如 StatusScope、SearchScope、DateRangeScope)叠加时,Page 必须放在最后。因为 Offset 和 Limit 是末端行为,若被前置 Scope 中的 Group、Having 或隐式 Order 干扰,会导致偏移错乱或 SQL 报错。
- 错误顺序:
db.Scopes(Page(p), StatusScope, DateRangeScope)——Page先执行,后续 Where 会追加到已带 Limit 的链上 - 正确顺序:
db.Scopes(StatusScope, DateRangeScope, Page(p)) - 如果用了
Preload,必须拆开:先查 ID 列表(带所有条件 + Page),再用db.Where("id IN ?", ids).Preload("Orders").Find(&users),否则关联加载会放大结果集
Page 结构体字段必须全是指针,且 Current/Size 要主动截断
Current 和 Size 不是指针会导致 Gin 的 ShouldBindQuery 在解析失败时静默设为 0,而 Offset(-10) 或 Limit(0) 会直接 panic;Total 必须是 *int64,因为它是输出字段,需在 Scope 内写入。
-
Current *int:判断if p.Current == nil || *p.Current -
Size *int:判断if p.Size == nil || *p.Size 100 { *p.Size = 20 }(硬上限防 DOS) - 别把
Total和Current放同一个结构体里绑定 query,否则前端传?page=1&size=20&total=999会污染
ORDER BY 不是可选项,而是分页稳定的唯一保障
没 Order 的分页在任何数据库上都不稳定。并发写入、MVCC、主从延迟都会让同一条 OFFSET/LIMIT 查询两次返回不同记录——这不是 bug,是 SQL 标准行为。
- 错误写法:
db.Scopes(...).Page(p).Find(&users)(无 Order) - 正确写法:
db.Order("id ASC").Scopes(...).Page(p).Find(&users),且id必须有索引 - 业务要求按时间排序?用
Order("created_at DESC, id DESC"),避免多条记录时间相同时排序漂移 - 别用
Order("RAND()")做随机分页,无法翻页且性能爆炸
最易被忽略的一点:游标分页在微服务中其实比 Offset 分页更合适——它天然规避了总数不准、深分页慢、排序漂移三大问题,但要求你放弃“总页数”这个前端幻觉,改用 last_id 或 cursor 字段驱动下一页。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











