<p>offset 应为 (page - 1) * size,limit 为 size;page ≥ 1 且 size ≤ 100;需用 int64 防溢出;避免结构体自动绑定导致 page=0;OFFSET 大时性能差,应改用游标分页。</p>

Go 里 offset 分页怎么算 offset 和 limit
offset 必须是 (page - 1) * size,不是 page * size;limit 就是 size。这是最常写错的点,第一页就会漏数据。
-
page必须 ≥ 1,0 或负数要直接拒绝,否则 MySQL 报ERROR 1064,SQLite 可能静默返回空结果 -
size建议硬限制 ≤ 100,防用户传size=10000拖垮数据库 - 用
int64算(page - 1) * size,避免溢出(比如page = math.MaxInt64) - 别依赖结构体自动绑定:如果前端传
page="abc",json.Unmarshal失败后默认设为 0,查出来的就是第 0 页——逻辑崩了还看不出错
为什么 OFFSET 越大越慢,且不能靠 Go 层优化
数据库执行 OFFSET 99999 时,并不是“跳过”,而是扫描前 10 万行再丢弃。即使有索引,ORDER BY + LIMIT + OFFSET 组合仍可能触发 filesort 和大量行读取。
- 百万级表翻到第 5000 页(OFFSET 100000),响应常超 2s,高并发下 DB CPU 直接拉满
- 这不是 Go 代码能改的,是 SQL 查询策略问题——换游标分页才是正解
- COUNT(*) 查总数 + OFFSET/LIMIT 查数据之间若有增删,页码和内容轻微不一致是常态,不用强求“绝对一致”
cursor 分页必须用 WHERE + ORDER BY + 严格单调字段
游标分页本质是把“第 N 页”转成“从某条记录之后取 N 条”,核心依赖排序字段的唯一性与单调性。
- 单字段如
id最稳妥;用created_at必须加二级排序,比如ORDER BY created_at DESC, id DESC,对应 WHERE 写成WHERE (created_at, id) < (?, ?) - 游标值必须校验:用
strconv.ParseInt(cursorStr, 10, 64),拒绝err == strconv.ErrSyntax或 ≤ 0 的值 - 第一页不带 cursor,SQL 是
SELECT * FROM t ORDER BY id DESC LIMIT 21(多查 1 条判断是否有下一页) - 返回下一页链接时,cursor 参数要用
url.Values构造,别字符串拼接,防注入和编码错误
Cursor 分页的隐藏一致性代价
它快、稳、适合无限滚动,但行为和 offset 分页有根本差异,容易被当成“升级替代”而忽略语义变化。
立即学习“go语言免费学习笔记(深入)”;
- 新插入的数据若排在当前游标“前面”,不会出现在已加载页面里——这不是 bug,是设计使然
- 删除中间某条记录,会导致后续页“跳过”一个 ID,但不会重复或错乱,因为游标只锚定已存在的边界点
- 不支持直接跳转任意页(比如“去第 100 页”),也不天然支持上一页;要实现上一页,得存 first_id 或反向查,逻辑更重
- 无法返回总页数,所以 UI 上不能显示“共 1234 页”,只能用“加载更多”或“没有更多”
游标分页不是银弹,它的优势集中在深度分页、高并发、数据高频变动场景;而 offset 分页在管理后台、固定页码跳转、需要总数统计的场合仍有不可替代性。选哪种,得看接口真实使用方式,而不是“听说 cursor 更先进”。


















