直接用 Offset + Limit 分页在 GORM 中不可靠,存在页码非法、总数与分页条件不一致、逻辑重复三大问题;应使用 Scopes 封装校验、计算与复用逻辑。

直接用 Offset + Limit 做分页在 GORM 里可行,但容易出错、难复用、不兼容总数统计逻辑——真正在项目里落地,得绕开裸写 offset 的坑。
为什么不能直接写 Offset((page-1)*pagesize)
看似简单,实际埋了三个雷:
-
page为 0 或负数时,Offset会传入负值,GORM 不报错但行为未定义(MySQL 可能忽略,SQLite 直接 panic) - 前端传参常以 1 起始页码,后端若没校验,
page=1时Offset(0)没问题,但page=0就崩了 - 总数统计和分页查询必须用完全一致的
Where条件,否则Count(*)和Find结果对不上——手写两遍条件极易漏改
用 Scopes 封装分页逻辑才靠谱
把分页参数校验、offset 计算、条件复用全收进一个函数里,既安全又可复用:
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}调用时链式拼接,条件只写一次:
var users []User
db.Where("status = ?", 1).
Scopes(Paginate(page, pageSize)).
Find(&users)
<p>var total int64
db.Model(&User{}).Where("status = ?", 1).Count(&total)注意:Count 必须用 Model(&T{}),不能复用带 Scopes 的查询实例,否则会把 Limit 也带进去,总数就错了。
Preload 分页时关联数据为空?
这是高频误操作:在分页查询里直接 Preload("Orders"),结果 Orders 是空的。原因很实在——GORM 的 Preload 是 N+1 查询,先查主表分页结果,再对每条记录单独查关联表;但分页后只拿到几条主记录,Preload 仍会发起 N 次查询,而如果关联表没匹配数据,字段自然为空。
真正要保证关联数据完整,得用 Joins + 去重:
db.Joins("JOIN orders ON users.id = orders.user_id").
Where("users.status = ?", 1).
Group("users.id").
Scopes(Paginate(page, pageSize)).
Find(&users)但注意:Joins 会导致主表字段被重复(一条 user 多个 order 就返回多行),必须加 Group,且确保 SELECT 字段不包含 order 的非聚合字段,否则 MySQL 会报错。
分页不是加两个参数就完事,关键在条件一致性、总数准确性、关联完整性这三点。很多人卡在 Preload 空数据上,其实问题不在 preload 本身,而在没意识到它和分页是两种不同查询语义。


















