不能直接复用同一gorm.DB实例分页,因Where/Order/Joins会修改其内部状态,导致Count()残留LIMIT等条件影响Find()结果;Page中Current、Size须为int,Total须为*int64以支持空值校验与默认填充。

为什么不能直接复用同一个 *gorm.DB 实例做分页
因为 Where、Order、Joins 这些链式调用会修改 *gorm.DB 的内部状态。如果你在分页 Scope 里先调 Count(),它会把 GROUP BY、HAVING 或 LIMIT 等临时条件“粘”进当前 DB 实例;紧接着执行 Find() 时,这些残留条件还在,要么查不到数据,要么 panic。
典型现象:db.Scopes(Page(p)).Find(&list) 返回空,但去掉 Page(p) 就能查出数据;或者 Total 比实际少——因为 Count() 被隐式加了 LIMIT。
必须每次在 Scope 内新建干净实例:
- 推荐:
db.Session(&gorm.Session{NewDB: true}),再调Count() - 更稳妥:
db.Model(&T{}).Where(...).Count(&total),显式指定模型和条件,不依赖当前链
Page 结构体里哪些字段必须是指针类型
Current 和 Size 必须是 *int,Total 必须是 *int64。
原因很实际:Gin 的 ShouldBindQuery 对非指针字段绑定失败时会静默设为零值。page=0 → Offset(-size) → GORM panic;size=0 → Limit(0) 失效,查出全表。
指针才能安全判空并填充默认值:
if p.Current == nil || *p.Current < 1 { *p.Current = 1 }if p.Size == nil || *p.Size < 1 || *p.Size > 100 { *p.Size = 20 }-
Total是输出字段,Scope 必须能写入,不能和输入参数混绑
分页 Scope 必须放在组合末尾
多个 Scope 组合使用(比如 StatusScope + DateRangeScope + Page(p))时,Page(p) 一定要最后调用。
否则前面的 Scope 可能已调过 Limit 或 Offset,导致分页逻辑被覆盖或错乱。例如 StatusScope 里误写了 db.Limit(1),后面再调 Page(p) 就完全失效。
还要注意:Order 必须显式声明,且排序字段要有索引。无 Order("id ASC") 的分页在主从、MVCC 场景下结果不可预测——同一页可能漏数据或重复。
微服务间复用分页中间件的关键约束
跨服务复用分页逻辑,不能靠全局变量或 context 传分片键、租户 ID 或排序字段。这些值必须作为参数明确传入 Scope 函数。
常见翻车点:
- 在
ShardByUserID里读context.Value("user_id")→ 并发下取到别的请求的 user_id - 在分页 Scope 里调
db.Unscoped()→ 后续所有 Scope 都跳过软删除 - 把
TableName()写在 model 上 → GORM schema 缓存冲突,查到错误分表
真正可复用的分页中间件,只做三件事:校验参数、算 Offset/Limit、独立查 Total。其他如分表、租户过滤、审计字段,应由各自服务的专用 Scope 承担,通过组合接入。


















