GORM无原生Paginate方法,所有同名封装均为第三方插件,存在参数不校验、Count查询未隔离、Order未强制等问题;推荐手写Limit+Offset或游标分页。

为什么不能直接用 Paginate 方法
GORM 本身没有 Paginate 函数,所有叫这个名字的封装都来自第三方库(比如 github.com/joshbetz/pagination),它们只是对 Limit + Offset 的语法糖。问题在于:这些插件不校验参数、不隔离 Count 查询、不强制 Order,升级 GORM 后还容易静默失效。
常见错误现象包括:page=0 不报错但查出空结果;size=0 在 SQLite 下可能返回全表;db.Where(...).Paginate(...) 的 Count 实际只统计了 limit 后的行数。
真正可控的方式是自己写 scope —— 它轻量、可调试、版本稳定,且能明确控制每一步行为。
Page 结构体必须带指针字段
分页参数不能用值类型接收,否则无法在 scope 内部安全修改默认值。比如前端没传 page,你得设为 1;没传 size,得设为 10。只有指针才能原地改写。
-
Current *int:允许判空后赋默认值,避免page 导致Offset(-10)panic -
Size *int:硬限制范围(如 1–100),超出则截断,防 DOS 攻击 -
Total *int64:必须是指针,因为Count(&total)要求地址,且需和分页结果共用同一结构体返回
别写 type Page struct { Current int } ,那样你只能靠外部 if 判断再重赋值,scope 里没法统一处理。
总数查询必须用独立 *gorm.DB 实例
db.Where("status = ?", "active").Limit(20).Offset(40).Count(&total) 返回的 total 是 20 —— 因为 Count 继承了前面的 Limit 和 Offset。这是 GORM 最隐蔽也最高发的 bug。
正确做法是新建一个干净实例:
countDB := db.Session(&gorm.Session{NewDB: true})
countDB.Model(&User{}).Where("status = ?", "active").Count(&total)
或者更稳妥地手写子查询(尤其在复杂 JOIN 场景):
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE u.status = ?) AS t", "active").Scan(&total)
别省这一步。线上一旦总数错,前端分页器就彻底失序。
Offset 分页只适用于小数据量或后台管理场景
MySQL 对 OFFSET 100000 LIMIT 20 的执行方式是:真实扫描前 100020 行,再丢掉前 100000 行。这不是 Go 层能优化的,是 SQL 引擎本身的限制。
当出现以下任一情况时,必须切游标分页:
- 用户端接口(App 列表无限滚动)
- 单表记录 > 50 万,且
page > 50 - 数据高频写入(新记录插入导致同一页重复/漏数据)
游标分页核心就三点:固定排序(如 ORDER BY id ASC)、索引覆盖(id 必须有主键或唯一索引)、前端传上一页最后一条的 id 值作为下一轮 WHERE id > ? 条件。它不依赖页码,也不需要总数,天然规避 Offset 性能陷阱。











