GORM无原生Paginate方法,必须手写Limit+Offset;需校验page≥1、pageSize∈(0,100],显式Order排序,总数查询须用独立db实例,大偏移量应切游标分页。

分页必须手写 Limit + Offset,GORM 没有 Paginate 方法
GORM 官方从未提供 Paginate 函数,v2 和 v2.6+ 均不支持。所谓“自动分页”要么是第三方封装(如 github.com/joshbetz/pagination),要么是项目里自己写的胶水代码——它们底层仍是调 Limit 和 Offset,但容易掩盖参数校验、排序缺失、Count 复用等关键问题。
直接手写最可控:
-
Offset((page - 1) * pageSize)必须在Limit(pageSize)之前调用(虽 v2 中顺序可互换,但固定写法更稳、更易被团队理解) -
pageSize传 0 或负数会导致查全表(SQLite 下)或 panic(MySQL/PostgreSQL) -
page为 0 或负数时,Offset计算会出负值,GORM 直接 panic;必须提前校验并归一化
参数校验不只防 crash,更是防 DOS 攻击
前端 URL 常见形如 /results?page=1&page_size=50,但不能直接透传给 GORM。用户可能传 page_size=10000、page=9999999,甚至 page=abc —— 这些都会触发慢查询、连接耗尽或 panic。
正确做法:
- 用
c.ShouldBindQuery(&p)绑定结构体,比c.Query()更安全,能统一做字段验证 -
PageNum≤ 0 时强制设为 1;PageSize超过上限(如 100)就截断:limit := min(p.PageSize, 100) - 非法输入(如非数字)应直接返回
400 Bad Request,别 fallback 到默认值 - 避免用
strconv.Atoi手动转——它不处理空字符串和溢出,ShouldBindQuery的binding:"gte=1,lte=100"标签更可靠
Order 不加,分页结果就不确定
没 Order 的分页是危险的:数据库不保证无序查询的行序,同一页可能漏数据、重复、甚至前后两次请求结果不一致——尤其在 MySQL 的 InnoDB 引擎下,MVCC + 索引扫描顺序变化极易暴露这个问题。
必须显式指定排序字段:
- 首选带索引的自增主键:
Order("id ASC") - 若需按时间分页,用组合排序防重复:
Order("created_at DESC, id DESC") - 别用
Order("updated_at")单字段——高并发下多个记录可能时间戳相同,导致分页错位 - 排序字段必须有索引,否则
Offset越大越慢(真实扫描前 N 行)
Count 查询必须独立 db 实例,否则总数永远不准
前端分页控件需要总条数来算页码数,但很多人写成这样:
db.Where(...).Offset(...).Limit(...).Count(&total)
这是错的:Count 会继承前面的 Offset 和 Limit,返回的是“当前页条数”,不是“符合条件的总条数”。
正确方式是另起一个干净的 *gorm.DB 实例:
- 用
db.Session(&gorm.Session{NewDB: true})创建新会话,再链式调用Where和Count - 或者直接复用原始
db变量(如config.DB),确保没被前面的Offset/Limit污染 - 别在同一个事务里混用 Count 和主查询——事务隔离级别可能让 Count 看不到未提交数据,而主查询能看到,造成总数与列表不一致
大偏移量(如 Offset > 100000)时,Count 本身也会变慢,这时得切游标分页,而不是硬扛 Offset。


















