必须校验分页参数:page<1时设为1,size硬限制1–100并截断;解析前判空;分页必加ORDER BY(推荐id ASC或created_at DESC + id DESC);Count需隔离会话或手写子查询;大数据量应改用游标分页。

必须校验 page 和 size,别信前端传的任何值
GORM 本身不校验分页参数,page=0、page=-1、size=999999 直接喂给 Offset/Limit,轻则查空,重则触发 MySQL 的 SQL ERROR 1235(如 OFFSET 超整型范围)或拖垮数据库。
-
page小于 1 时强制设为1,不能跳过或返回错误——用户点“第 0 页”大概率是手误,不是要报错 -
size必须硬限制(如 1–100),超出就截断(比如设成20),而不是拒掉请求——防 DOS,也避免前端 bug 导致后端 panic - 用
strconv.Atoi解析时,空字符串、非数字都会报错,得先c.Query("page")再判空,别依赖http.ParseForm()
Offset + Limit 之前,Order BY 是刚需
没 ORDER BY 的分页等于抽签:同一查询执行两次,可能漏掉记录或重复返回。MySQL/PostgreSQL 不保证无序 LIMIT 的稳定性,尤其在有并发写入时。
- 排序字段必须带索引,优先选主键(
id ASC)或带索引的时间字段(created_at DESC) - 避免只用
created_at排序——如果多条记录时间相同,顺序不可控;加个id DESC当二级排序更稳 - 别写
db.Order("RAND()").Limit(10)做随机分页,性能爆炸且无法翻页
总数 Count 不能和分页共用一个 *gorm.DB 实例
db.Where(...).Limit(10).Offset(20).Count(&total) 返回的 total 是加了 LIMIT 后的条数,永远 ≤ 10 ——这是 GORM 最常被踩的坑。
- 用
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total)隔离会话,防止复用原链的 Limit/Offset - 复杂 JOIN 查询时,
Count容易出错,直接手写子查询更可靠:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE ...) AS t").Scan(&total) - 如果业务允许(比如不要求精确总页数),可只返回
has_next布尔值,查size + 1条,省掉 COUNT
大数据量下,OFFSET 分页会越来越慢,这不是 GORM 的问题
当 OFFSET 超过 10 万行,MySQL 得扫描并丢弃前 N 行,响应从毫秒变秒级。这时候优化重点不在 Go 层,而在 SQL 和索引设计。
立即学习“go语言免费学习笔记(深入)”;
- 游标分页(Cursor-based)更高效:前端传上一页最后一条的
created_at和id,后端查WHERE created_at - 覆盖索引能极大缓解 OFFSET 压力,比如
SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 100000,只要id在索引里,就不回表 - 别幻想“GORM 自动优化 OFFSET”,它只是拼 SQL 的工具,慢的根本原因是 MySQL 扫描逻辑
真正难的不是写对 Limit 和 Offset,而是想清楚:你这个分页到底要不要精确总数?能不能接受游标?数据写入频不频繁?这些决定了该用哪条路,而不是抄个代码就完事。


















