游标分页应避免OFFSET,核心是用排序字段值替代页码;需确保WHERE条件正确(如WHERE id > ?)、参数安全绑定、游标编解码,三者缺一不可。

直接结论:别用 OFFSET 做深度分页,Go 里实现游标分页的核心是「用排序字段值代替页码」,关键在 WHERE 条件构造、参数安全绑定、游标编码解码三处,错一个就漏数据或 panic。
WHERE 条件怎么写才不漏不重
游标分页不是“跳过前 N 行”,而是“从某条记录之后开始取”。必须依赖有索引、严格单调(或全局唯一)的排序字段组合。
- 单字段 ID(自增主键)最简单:
WHERE id > ? ORDER BY id ASC LIMIT ?,首次请求不带 WHERE - 时间戳 + ID 更稳妥(防并发写入重复):
WHERE (created_at, id) > (?, ?) ORDER BY created_at ASC, id ASC LIMIT ?,注意 PostgreSQL/MySQL 8.0+ 支持行值比较,SQLite 得拆成:WHERE created_at > ? OR (created_at = ? AND id > ?) - 用
DESC排序时,条件要反向:比如ORDER BY created_at DESC, id DESC,对应WHERE (created_at, id) ,否则查不到数据 - 千万不能用
created_at单字段当游标——同一毫秒插入多条,WHERE 条件会漏掉后面几条
游标值怎么解析和编码才安全
游标是客户端传来的字符串,不可信。直接 json.Unmarshal 或 strconv.ParseInt 会 panic,拼进 SQL 更是高危。
- 解析时必须分步校验:
url.QueryUnescape→ 拆分(如按:)→time.Parse时间部分 →strconv.ParseIntID 部分,任一失败就拒绝请求 - 编码推荐 Base64:
base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d", t.Format(time.RFC3339), id))),避免 URL 中出现+、/、空格等歧义字符 - 别用 Unix 时间戳整数当游标——时区、精度、负值都容易出问题;
time.RFC3339字符串更直观可控 - 返回的
next_cursor是空字符串,不等于“没下一页”——只说明当前页数据不足pageSize条,需靠多查 1 条来判断(见下一条)
为什么一定要多查 1 条
只查 LIMIT pageSize,你永远不知道有没有下一页。服务端需要明确告诉前端:“这页之后还有数据”,否则滚动到底部会提前终止。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- 实际执行时用
LIMIT pageSize + 1,比如每页 20 条,就查 21 条 - 拿到结果后,如果长度 ==
pageSize + 1,说明有下一页,把第 21 条的排序字段值编码为next_cursor;否则next_cursor设为空 - 响应体只返回前
pageSize条,第 21 条仅用于游标生成,不暴露给前端 - 这个逻辑必须在业务层做,不能交给数据库或 ORM 自动处理——GORM 的
Limit不管你多查一条的意图
GORM 里怎么绕过 Offset 写游标查询
GORM 没内置游标分页支持,但可以干净地绕过 Offset,关键是手动拼 WHERE、禁用自动 LIMIT/OFFSET。
- 不要调用
.Offset(),它本质还是生成OFFSETSQL;用.Where("id > ?", lastID)替代 - 排序必须显式写:
.Order("id ASC"),GORM 不保证默认顺序,不写会出错 - 慎用
Preload:分页时Preload("Orders")会导致 N+1 查询,且无法控制关联数据量;应先查主表 ID 列表,再用IN批量查关联 - 如果非要用插件,
github.com/joshbetz/pagination专为 GORM v2 设计,但底层仍是手写 WHERE,不如自己封装一个CursorPage函数来得透明
最容易被忽略的是游标生命周期管理:游标值本身不带过期机制,也不校验是否属于合法数据范围。比如客户端传一个极小的 id=1,而真实数据最小 id=1000,查询会返回全表——必须在 WHERE 后加兜底条件(如 AND id >= 1000)或做存在性校验,否则就是个潜在 DoS 点。

















