必须校验page和size参数,page

必须校验 page 和 size,否则后端直接裸奔
GORM 不会帮你拦非法参数,page=0、page=-1、size=999999 这些值一旦进 SQL,轻则查空,重则触发 MySQL 的 OFFSET 性能雪崩甚至 OOM。别指望前端传得“规矩”——它可能根本没传、传了字符串、传了超大数。
-
page小于 1 时强制设为 1,不接受 0 起始页(语义混乱且易错) -
size必须硬限制范围,比如 1–100;超出就截断(如设为 20)或返回400 Bad Request - 用
strconv.Atoi转换时,空字符串、非数字都会报错,要先判空再转,别直接panic - 别用
r.ParseForm(),r.URL.Query().Get("page")更轻量、更可控
Offset + Limit 本身不保证分页稳定,漏数据是常态
写 db.Offset(100).Limit(10).Find(&users) 看似没问题,但只要表有并发写入、没加 ORDER BY,同一请求多次执行很可能跳过或重复记录。MySQL/PostgreSQL 对无序分页不做一致性承诺,这不是 GORM 的锅,是 SQL 语义决定的。
- 必须显式加
.Order("id ASC")或.Order("created_at DESC, id DESC")—— 主键或带时间戳的唯一组合最稳 - 避免只用
.Order("created_at DESC"),当多条记录时间相同时,排序结果不确定 - 不要省略
ORDER BY去“优化”,这是拿数据正确性换毫秒级性能,得不偿失
总数查询不能和分页共用一个 db 链式调用
db.Where(...).Limit(10).Offset(20).Count(&total) 算出来的不是总条数,而是“带 limit/offset 后的结果数”,永远 ≤10。GORM 的 Count 会复用前面链上的 LIMIT 和 OFFSET,这是高频误用点。
- 简单场景:用
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total),靠NewDB: true隔离上下文 - 复杂关联查询(含
Joins、Preload):手写子查询更可靠,例如db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN ... WHERE ...) t").Scan(&total) - 别在事务里反复调
Count+Find,两次查询之间若数据变更,总数和当前页数据可能对不上(业务可接受就另说)
数据量大了,OFFSET 分页必然变慢,得换思路
当 OFFSET 超过 10 万行,MySQL 往往要扫描并丢弃前 N 行,响应从 20ms 拉到 2s+。这不是 GORM 层能改的,是索引和查询模型问题。
- 游标分页优先:前端传上一页最后一条的
created_at和id,SQL 改成WHERE created_at - 覆盖索引辅助:确保
ORDER BY字段 + 查询条件字段都在联合索引里,避免回表 - 总数可妥协:超 100 万条时,“共 XXX 条” 可降级为估算值或隐藏,用户真正需要的是“下一页”,不是精确总数
分页看着简单,但稳定性和性能陷阱全藏在参数校验、排序约束、总数获取、大数据应对这四个环节里——少踩一个,线上就可能出一次漏数据或慢查询告警。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











