GORM分页需手动组合Limit和Offset,无内置Paginate方法;Offset表示跳过条数而非页码,正确换算为(page-1)*size,且必须校验参数、显式排序、独立查总数,高偏移场景应改用游标分页。

直接上结论:GORM 的分页不是靠某个“分页函数”自动完成的,而是用 Limit 和 Offset 手动组合实现;新手最容易错的是把 Offset 当成“页码”,其实它对应的是“跳过多少条”,页码需自行换算。
分页核心:Limit + Offset 怎么配才不翻车
GORM 没有内置 Paginate 方法(v2 仍不提供),所有分页逻辑必须自己算。常见错误是写成 db.Offset(page).Limit(size),结果第 1 页就漏掉前 page 条数据。
-
Offset(n)表示跳过前 n 条记录,不是“第 n 页”——所以第 1 页应传0,第 2 页传size,第 3 页传2 * size - 正确换算公式:
offset = (page - 1) * size,其中page从 1 开始计数(用户友好),size是每页条数 - 务必对
page和size做校验:防止负数、超大值(如size > 100)或非整数导致 SQL 报错或性能崩塌
带总数的分页:Count 查询为什么不能复用主查询
前端常需要“共 X 条,第 Y 页”,这就得查总数。但别试图用 db.Session(&gorm.Session{DryRun: true}).Count() 或拼接子查询——GORM v2 的 Count 会忽略 Order、Joins 甚至部分 Where 条件,导致总数不准。
- 安全做法:用原生 SQL 查总数,例如
db.Raw("SELECT COUNT(*) FROM (?) t", subQuery).Scan(&total),其中subQuery是你主查询去掉Limit/Offset后的*gorm.DB - 如果没 Join 复杂条件,可用
db.Where(...).Model(&User{}).Count(&total),但必须确保Where条件和主查询完全一致 - 注意:PostgreSQL 对
COUNT(*)优化好,MySQL 在大表上可能慢,此时建议加覆盖索引,比如(status, created_at)配合常用查询条件
避免 OFFSET 深分页性能暴跌
当 page 很大(比如第 10000 页),OFFSET 99999 会让数据库扫过前 10 万行再取结果,响应直接卡死。
- 替代方案是“游标分页(Cursor-based Pagination)”:用上一页最后一条记录的排序字段值(如
id或created_at)作为下一页起点,例如WHERE id > ? ORDER BY id LIMIT ? - GORM 写法:
db.Where("id > ?", lastID).Order("id ASC").Limit(size).Find(&users),无需Offset,性能稳定 - 缺点:不支持跳转任意页,只能逐页或回溯;要求排序字段严格唯一且有索引(推荐用主键
id)
封装一个可复用的分页结构体时要注意什么
很多人封装 Pagination 结构体,但容易忽略两个关键点:错误透出和链式调用中断。
- 不要在封装里直接调用
Find或Count并吞掉error——应该返回error让上层处理,否则查不到数据时静默失败 - 避免写成
db.Scopes(Paginate(page, size)).Find()这种形式,因为Scopes无法同时注入Count逻辑;更稳妥的是返回func(*gorm.DB) *gorm.DB闭包,由调用方决定何时 Count / Find - 字段命名别用
PageNum或PageSize,直接叫Page和Size,和 GORM 官方示例风格一致,减少认知成本
最易被忽略的一点:分页参数永远来自 HTTP 查询字符串,但 Go 的 net/http 默认把所有值当字符串,strconv.Atoi 失败时若不返回 400 错误,后端就会用默认值(比如 page=1)硬查,前端看到的“第 0 页”其实是第 1 页,排查时绕一大圈。


















