pagination结构体字段必须用指针类型,因为shouldbindquery对非指针字段绑定失败时静默设零值:page=abc使current变为0(非法页码致panic),size=0使limit(0)失效查全表;而*int可区分“未传参”(nil)与“传非法值”(非nil但无效)。

为什么 Pagination 结构体字段必须用指针类型
因为 Gin 的 ShouldBindQuery 对非指针字段绑定失败时会静默设为零值:比如 page=abc 会导致 Current 变成 0,而 0 是非法页码(Offset(-size) 直接 panic);size=0 则让 Limit(0) 失效,查出全表。
用指针能区分“未传参”和“传了非法值”:
-
Current *int:可判断p.Current == nil或*p.Current ,再安全设默认值 <code>1 -
Size *int:同理,if p.Size == nil || *p.Size -
Total *int64:必须指针,它是输出字段,Scope 要写入它;且不能和Current/Size混在同一个结构体里直接绑定 query,否则前端?page=1&size=20会把Total也当输入参数解析
Scopes 封装分页时,为什么不能复用同一个 *gorm.DB 实例
因为 GORM 链式调用(如 Where、Joins)会修改原 *gorm.DB 实例的内部状态。你在分页 Scope 里调用 Count(&total),它可能悄悄加了 GROUP BY 或吃了 ORDER BY,污染后续的 Find() 查询。
常见错误现象:
-
db.Scopes(Paginate(p)).Find(&list)返回空,但单独db.Find(&list)能查出数据 -
Total比实际少,因为Count()被隐式加了LIMIT
正确做法有两种:
- 在 Scope 内新建干净实例:
db.Session(&gorm.Session{NewDB: true}).Model(&T{}).Where(...).Count(&total) - 显式用
Model+Where独立查总数:db.Model(&T{}).Where(db.Statement.Clauses).Count(&total)(更可控)
分页 Scope 必须放在 Scopes 组合的末尾
如果你组合多个 Scope,比如 StatusScope、DateRangeScope、Paginate,Paginate 必须最后调用。否则前面的 Scope 可能提前触发 Count() 或干扰 Offset/Limit 计算逻辑。
例如错误写法:db.Scopes(Paginate(p), StatusScope).Find(&list) —— StatusScope 在后,可能覆盖或忽略分页条件。
正确顺序:
db.Scopes(StatusScope, DateRangeScope, Paginate(p))- 所有前置 Scope 应只做
Where、Joins等条件追加,不调用Count或Find - 确保
Paginate内部只读取当前Statement.Clauses,不修改原始链
Order 必须显式声明,且字段要有索引
GORM 不保证无序查询的行序稳定。尤其在主从复制、MVCC 或并发写入场景下,同一条 Offset/Limit 查询两次可能返回不同记录——漏数据或重复。这不是 bug,是数据库行为。
错误写法:db.Scopes(Paginate(p)).Find(&users)(没 Order)
正确写法:
-
db.Order("id ASC").Scopes(Paginate(p)).Find(&users),且id字段必须有索引 - 若按时间排序,慎用
created_at:高并发下可能重复,建议组合created_at, id(如Order("created_at DESC, id DESC")) - 别依赖数据库默认排序,哪怕表只有单字段主键
真正容易被忽略的是:Order 字段的索引不是“有就行”,而是要匹配查询模式。比如用 Order("status, created_at DESC"),就得建联合索引 (status, created_at),否则分页性能会断崖下跌。











