GORM无内置分页,需手写Limit+Offset:校验page≥1、pageSize∈(1,100],显式Order排序,总数查询须用独立db.Session实例隔离,大偏移量应切游标分页。

GORM 没有内置分页组件,所谓“轻量级分页组件”只能是自己封装的 Limit + Offset 调用组合,加参数校验和总数查询隔离——别被“组件”二字带偏,它本质就是几行可控的 SQL 控制逻辑。
怎么写一个真正可用的分页函数
核心就三件事:算 offset、查数据、查总数。不能把这三步塞进一个链式调用里,否则 Count 会受 Limit 和 Offset 污染。
-
Offset((page - 1) * pageSize)才是第page页的正确跳过量,page必须 ≥ 1,否则Offset传负数会 panic - 总数必须用独立的
*gorm.DB实例查:db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total) - 如果带
Preload或复杂Joins,Count很可能不准,此时直接手写子查询更稳:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users WHERE ...) AS t").Scan(&total)
为什么 Order BY 不是可选项
没 Order 的分页在并发写入场景下必然漏数据或重复——这不是 GORM 的 bug,是 SQL 标准行为。MySQL/PostgreSQL 对无序 LIMIT 不保证结果集稳定性。
- 必须显式写
Order("id ASC")或Order("created_at DESC, id DESC"),二级排序防时间冲突 - 别用
Order("RAND()")做“随机分页”,它无法翻页且性能极差 - 排序字段必须有索引,否则
OFFSET越大越慢,不是 Go 层能优化的
前端传 page=0 或 size=99999 怎么办
不能信任何前端输入。GORM 不做参数校验,全靠你手动兜底。
-
page≤ 0 时强制设为 1,别返回错误——用户点错页码不是系统故障 -
pageSize必须硬限制(如 1–100),超限就截断:size := min(p.Size, 100),别让恶意请求拖垮 DB - 用
c.ShouldBindQuery(&p)绑定结构体比c.Query()更安全,能统一做binding:"required,min=1"验证 - 空字符串、非数字值会导致
strconv.Atoi报错,必须先判空再转
什么时候该放弃 Offset 分页
当单表记录超 10 万、且 OFFSET 值经常 > 50000 时,MySQL 就得扫描并丢弃前 N 行,响应从毫秒变秒级。这时候封装再“轻量”也没用。
- 游标分页才是解法:前端传上一页最后一条的
created_at和id,后端查WHERE created_at - 游标分页无法跳转任意页,但对“加载更多”场景足够,且性能恒定
- 如果你的业务真需要精确总页数 + 跳页,那就得接受慢查,或者加覆盖索引缓解(如
INDEX(created_at, id))
最易被忽略的一点:分页不是“查完再数”,而是“先数再查”,且两次查询的条件必须完全一致,连 WHERE 子句都不能少一个空格——否则总数和列表对不上,前端页码就乱了。


















