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

Go语言用GORM做分页,没有现成的Paginate方法可用——这是最常被踩的坑。官方只推荐手写Limit + Offset,或在大数据量下改用游标分页。
为什么不能直接用 Paginate 方法
Paginate不是GORM内置函数,是第三方插件(如github.com/joshbetz/pagination)提供的封装。它底层仍是调Limit/Offset,但会掩盖参数校验问题:比如传page=0不报错、pageSize=0可能查出全表(SQLite下)、Offset负数直接panic。GORM v2.6+还因Scope行为变更导致老版本插件失效或漏数据。
- 插件不解决根本性能问题,只是语法糖
- 不同GORM小版本间兼容性差,升级后容易静默失败
- 它不会帮你加
Order,也不会隔离Count查询,这些还得自己补
手写 Limit + Offset 的正确姿势
核心就三件事:校验参数、显式排序、分开查总数。别跳步,少一个都会线上出事。
-
page必须≥1,pageSize必须>0且≤100(硬限制防DOS),用strconv.Atoi前先判空字符串 -
Offset((page - 1) * pageSize)必须写在Limit(pageSize)之前(虽v2中顺序可互换,但固定写法更稳) - 必须加
Order("id ASC")或Order("created_at DESC, id DESC")——没Order的分页结果不可靠,同一页可能重复或漏数据 - 总数不能用
db.Where(...).Limit().Offset().Count(),它返回的是limit后的条数;要另起一个db.Session(&gorm.Session{NewDB: true})实例去查
什么时候必须放弃 Offset 分页
当Offset超过10万,或者业务要求“无限滚动”“实时加载”,就得切游标分页。MySQL/PostgreSQL对大偏移量的处理是真实扫描并丢弃前N行,不是跳指针。
立即学习“go语言免费学习笔记(深入)”;
- 游标分页依赖确定排序字段,首选带索引的自增
id,次选created_at+id组合(避免时间重复) - SQL形如
WHERE id > ? ORDER BY id LIMIT 20,前端传last_id而非page - 首次请求查
ORDER BY id ASC LIMIT 20,后续每页取users[len(users)-1].ID作为下一轮last_id - 别在游标分页里混用
Preload,关联数据应分两步:先查主表ID列表,再用IN批量加载
Count 查询容易被忽略的细节
总数查不准,八成是因为复用了同一个*gorm.DB链。GORM的Count会继承前面的Limit/Offset,但你往往没意识到。
- 错误写法:
db.Where("status = ?", "active").Limit(20).Offset(1000).Count(&total)→total永远≤20 - 正确写法:先构建条件
cond := db.Where("status = ?", "active"),再分别调cond.Count(&total)和cond.Offset().Limit().Find() - 复杂JOIN场景下,
Count易出错,直接手写子查询更可靠:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE ...) t").Scan(&total) - 超百万数据且总数不敏感时,可省掉
COUNT,只查pageSize + 1条,用是否满额判断HasNext
真正难的不是写几行Limit/Offset,而是参数校验、排序稳定性、总数隔离、大偏移应对这四点——漏掉任意一个,上线后都可能变成凌晨三点的告警。


















