分页必须显式校验page/size、强制设默认值、限制范围;Order为必需项以保证稳定性;Count需独立查询;大数据量须用游标分页。

分页参数不校验就用,GORM 会直接 panic 或生成非法 SQL —— 没有“默认值兜底”这回事,必须手动设、手动截、手动判空。
page 和 size 必须显式校验并设默认值
前端传 page=0、page=-1、size=abc、size=0 是常态,GORM 不做任何拦截。它会原样拼进 SQL:OFFSET -20 或 LIMIT 0,前者 MySQL 直接报错 ERROR 1235,后者在 SQLite 下可能查出全表。
-
page小于 1 时,强制赋值为1(别返回 400,用户点错页码不是接口错误) -
size必须限制在1–100区间,超出就min(p.Size, 100)截断(防 DOS,也避免慢查询拖垮 DB) - 用
c.Query("page")拿值前先判空:r.URL.Query().Get("page") == ""→ 设默认;非数字字符串不能忽略,strconv.Atoi前必须if !unicode.IsDigit(rune(s[0]))或用ShouldBindQuery并检查 error
Order BY 不是可选,而是分页稳定性的前提
没 Order 的分页等于随机抽签:同一条 Limit/Offset 查询执行两次,可能漏数据、重复、甚至返回空 slice 且 err == nil。这不是 GORM bug,是 MySQL/PostgreSQL 的标准行为——无序结果集不保证稳定性。
- 必须显式写
Order("id ASC")或Order("created_at DESC, id DESC");单用created_at不够,高并发下毫秒级重复会导致排序漂移 - 避免
Order("status ASC")这类低区分度字段,索引无效且内部排序不可控 -
Order("RAND()")禁止用于分页,无法支持下一页,且全表扫描性能爆炸
Count(*) 必须独立执行,不能复用同一个 *gorm.DB 实例
db.Where("x = ?", v).Limit(10).Offset(20).Count(&total) 查出来的 total 永远 ≤ 10 —— 因为 GORM v2 的 Count 会复用链上的 Limit/Offset,这不是 bug,是设计如此。
- 正确做法:先构建干净查询实例,例如
query := db.Where("x = ?", v),再分别调用query.Count(&total)和query.Order("id ASC").Limit(size).Offset((page-1)*size).Find(&list) - 更稳妥的是用
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total),彻底隔离上下文 - 复杂 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)
大数据量必须切游标分页,Offset 分页不是“能用就行”
当 OFFSET 超过 10 万,MySQL 实际要扫描并丢弃前 N 行,响应时间从 20ms 拉到 1.5s+。这不是代码问题,是存储引擎的物理限制。
- 游标分页不是“高级技巧”,而是 >10 万行场景下的强制要求;用
WHERE id > ? ORDER BY id ASC LIMIT 20替代OFFSET - 前端需传上一页最后一条的
id(或组合字段),后端不再解析page参数 - 游标分页天然不支持跳页,但换来的是 O(1) 查询性能和强一致性
最容易被忽略的是:所有校验逻辑必须在调用 Offset 和 Limit 之前完成,且 Order 必须出现在 Limit 之前(虽然 v2 中顺序可互换,但固定写法才能跨版本稳定)。一旦漏掉任一环,线上就可能出现数据错乱或雪崩。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











