游标分页不能用 OFFSET + LIMIT,因其基于上一页最后记录状态续读,而 OFFSET 越深越慢且高并发下易漏数或重复;应使用排序字段值(如 created_at + id)作为 cursor,通过行值比较查询,并注意时区、编码与参数绑定安全。

游标分页为什么不能用 OFFSET + LIMIT
因为游标分页本质是“基于上一页最后一条记录的状态继续读”,不是跳过前 N 条。用 OFFSET 会导致翻页越深越慢,且在高并发写入场景下容易漏数据或重复——比如中间插入/删除了记录,OFFSET=1000 下一页可能跳过新插入的几条,或把刚删掉的 ID 又算进来。
Go 后端实现游标分页,核心是:客户端传一个「上次最后一条的排序字段值」(比如 cursor=12345),服务端查「比它更新」的前 N 条,再把新结果的最后一条该字段值作为下一次 cursor 返回。
常见错误现象:
• 客户端传 cursor=0 或空字符串,没做校验直接拼 SQL,导致查出全表
• 用时间戳当游标时,没处理同一毫秒内多条记录(必须加二级唯一键,如 id)
• 返回的 next_cursor 是空字符串或 null,前端误判为“无下一页”,其实只是最后一页没数据可提 cursor
用 time.Time + id 复合游标最稳妥
单一时间戳不可靠:数据库写入时间可能相同,尤其批量导入或高并发。推荐组合 created_at 和 id(主键自增或 UUID),确保严格单调。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
• 查询条件写成: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 > ?)
• 解析游标时,用 url.QueryUnescape + time.Parse + strconv.ParseInt 分步校验,别直接 json.Unmarshal 到结构体里——游标是客户端传的,不可信
• 游标值建议 Base64 编码(如 base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%d", t.Format(time.RFC3339), id)))),避免 URL 中出现特殊字符或时区歧义
database/sql 查询时如何安全绑定游标参数
别把游标拼进 SQL 字符串,必须用参数占位符。尤其复合游标,要拆成两个变量传进去。
示例片段:
rows, err := db.Query(`
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) > (?, ?)
ORDER BY created_at ASC, id ASC
LIMIT ?`,
cursorTime, cursorID, pageSize+1, // 多查 1 条判断是否有下一页
)
if err != nil {
return nil, err
}注意:
•
pageSize+1 是关键——只查 pageSize 条,就无法知道是否还有下一页• 遍历
rows 时,存下最后一条的 created_at 和 id,用于生成 next_cursor• 如果查到的条数
,说明没下一页,<code>next_cursor 设为空字符串(不是 null)
客户端传错游标或服务端解析失败怎么处理
游标是客户端控制的 opaque token,服务端不负责“修复”,只做严格校验和快速失败。
建议策略:
• 所有解析失败(格式错、时间非法、ID 非数字)统一返回 400 Bad Request + 明确错误信息,如 "invalid cursor: malformed timestamp"
• 不 fallback 到第一页,避免隐藏问题
• 日志中记录原始游标值(脱敏后),方便排查前端生成逻辑错误
• 如果业务允许“首次无游标”,就约定 cursor 参数缺失或为空时,按 WHERE created_at > '0001-01-01' 起查,但必须显式判断 cursor == "",不能依赖零值
最容易被忽略的是时区。PostgreSQL 默认用 UTC 存 timestamp with time zone,但 Go 的 time.Time 解析 RFC3339 时带本地时区。务必统一用 time.UTC 解析和格式化,否则游标在跨时区客户端间会错乱。

















