用Limit+Offset分页需配合Count总数查询、参数校验和边界保护,否则Offset过大导致MySQL全表扫描、CPU飙升、超时;应限制Page≤1000、Size≤100,并用validator校验输入。

直接说结论:用 Limit + Offset 是最稳妥的起步方式,但必须配合 Count 查询总数,且不能跳过参数校验和边界保护。
为什么不能只写 Limit 和 Offset 就完事?
很多人照着示例一写就跑,结果线上出问题:页面卡死、MySQL CPU 暴涨、翻到第 100 页直接超时。根本原因是没意识到两个事实:
-
Offset越大,数据库越要扫描前面所有行——OFFSET 100000不是跳过 10 万条记录,而是让 MySQL 先读、再丢弃 10 万条 -
Count(*)在无索引或大数据量下也会变慢,尤其带WHERE条件时
所以实际分页不是“拼 SQL”,而是“控制查询成本”。建议默认加限制:Page ≤ 1000,Size ≤ 100,并在日志里埋点记录高偏移请求。
ShouldBindQuery 绑定分页参数时容易漏掉什么?
用 Gin 的 ShouldBindQuery 接收 page 和 size 很方便,但几个关键点常被忽略:
- 没设默认值:前端不传
page时结构体字段为 0,Offset(-1 * size)会 panic - 没做范围检查:
size传个 10000,一次查上万条数据,内存和网络都吃不消 - 没处理负数或非数字:比如
?page=abc,ShouldBindQuery默认静默失败(字段保持零值),而不是报错
推荐写法:
type PageQuery struct {
Page int `form:"page" binding:"required,min=1,max=1000"`
Size int `form:"size" binding:"required,min=1,max=100"`
Keyword string `form:"keyword"`
}然后用 c.ShouldBindQuery(&q),靠 validator 自动拦截非法输入。
总数统计 Count 怎么写才不出错?
总数必须和实际数据查询用**完全一致的 WHERE 条件**,否则前后端分页错位。常见错误是:
- 在
Count时漏了软删除字段(如deleted_at IS NULL) - 对
Keyword做了LIKE模糊匹配,但Count没加,导致总数不准 - 用
db.Model(&User{}).Where(...).Count(&total),但Model指向了空结构体,GORM 可能忽略表名或别名
安全写法是复用同一个 *gorm.DB 实例:
db := config.DB.Where("deleted_at IS NULL")
if q.Keyword != "" {
db = db.Where("name LIKE ?", "%"+q.Keyword+"%")
}
var total int64
db.Count(&total).Error // 注意:Count 返回的是 int64
<p>var users []User
db.Limit(q.Size).Offset((q.Page - 1) * q.Size).Find(&users)什么时候该换游标分页(cursor pagination)?
当业务出现以下任一情况,就该考虑放弃页码制,改用游标:
- 数据实时写入频繁(如日志、消息流),用户翻页时新数据插入导致“重复/丢失”条目
- 需要稳定响应时间,不能接受
OFFSET随页码线性变慢 - 前端明确支持游标(比如无限滚动列表)
游标本质是“记住上一页最后一条的排序字段值”,例如按 id DESC 分页,第二页请求就带 ?cursor=12345,SQL 变成 WHERE id 。它不依赖总页数,也不怕中间插入,但要求排序字段**必须有唯一索引且不允许 NULL**。
真正难的不是写出分页代码,而是判断当前接口到底适不适合用页码制——很多团队直到监控里看到 slow_query 超过 2s 才想起这事。


















