gorm无原生分页,须手写limit+offset或游标分页;必须校验page≥1、pagesize∈(0,100],显式order排序,总数查询需独立db实例,大偏移量应切游标分页。

GORM 本身不支持分页,所谓“多卡槽机房设备分页”只是业务场景描述,底层仍要靠 Limit + Offset 或游标实现——别被业务名词带偏,重点是参数校验、排序稳定、总数隔离这三件事。
怎么算 Offset 才不会跳页或漏数据
Offset 不是“第几页”,而是“跳过多少条”。第 3 页、每页 20 条,得写 Offset(40),不是 Offset(3)。常见错误是直接用前端传的 page 值去调 Offset(page),结果第 1 页就空了。
- 必须统一转换:offset := (p.PageNum - 1) * p.PageSize
-
PageNum≤ 0 时强制设为 1,别返回错误(用户点错页码很常见) -
PageSize必须限制范围(如 1–100),超限就截断,防恶意刷库 - 用
strconv.Atoi前先判空字符串,否则c.Query("page")返回 "" 会 panic
为什么加了 Limit 和 Offset 还会重复或丢记录
根本原因是没加 Order。数据库对无序查询不保证行序,尤其在有并发插入/删除时,同一分页请求执行两次可能返回不同结果。这不是 GORM 的 bug,是 SQL 语义本身决定的。
- 必须显式写
Order("id ASC")或Order("created_at DESC, id DESC") - 避免只用
Order("created_at DESC"):时间字段重复时排序不可控 - 排序字段必须有索引,否则
OFFSET越大越慢(MySQL 要扫描并丢弃前 N 行) - 别用
Order("RAND()")做“随机分页”,无法翻页且性能爆炸
总数查不准?大概率是复用了同一个 *gorm.DB 实例
db.Where(...).Limit(20).Offset(40).Count(&total) 返回的永远 ≤ 20,因为 Count 继承了前面的 Limit 和 Offset。这是 GORM 分页里最高频的逻辑错误。
- 简单场景:用
db.Session(&gorm.Session{NewDB: true}).Model(&Device{}).Where(...).Count(&total) - 复杂 JOIN 查询:手写子查询更可靠,例如
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM devices d JOIN racks r ON d.rack_id = r.id WHERE ...) t").Scan(&total) - 如果业务允许(比如不要求精确总页数),可只查
pageSize + 1条,用len(results) > pageSize判断是否有下一页
什么时候必须放弃 Offset 分页
当设备表超过 50 万行,或单次 OFFSET 超过 10 万,响应时间就会从毫秒级跳到秒级。这不是 Go 层能优化的,是 MySQL 扫描机制决定的。此时游标分页不是“可选”,而是“必须”。
- 游标依赖确定排序字段:首选主键
id,次选created_at + id组合(避免时间重复) - SQL 形如
WHERE id > ? ORDER BY id LIMIT 20,前端传上一页最后一条的id - 首次请求用
ORDER BY id ASC LIMIT 20,后续取users[len(users)-1].ID作为下一轮游标 - 别在游标分页里混用
Preload,关联数据应分两步:先查 ID 列表,再用IN批量加载
最易被忽略的是:排序字段没索引、Count 复用链式 DB、以及把 page 当 Offset 直接用——这三个点线上出事概率最高,而且问题现象分散(有时漏数据,有时慢,有时总数为 0),排查起来特别耗时间。











