Rows不支持分页,需用WHERE+排序字段实现流式游标分页;必须建索引、校验游标、显式关闭rows并检查错误。

Rows 本身不支持分页,它只是原始的 *sql.Rows 流式迭代器;所谓“流式分页”,本质是用 Rows + 自定义游标逻辑替代 OFFSET,靠业务字段(如 id 或时间戳)做边界控制。
为什么不能直接对 Rows 做 LIMIT/OFFSET 分页
因为 Rows 是查询执行后返回的只读游标,不携带 SQL 构建能力。GORM 的 Rows() 方法底层调用的是 db.Session().Raw(sql).Rows(),它只负责把结果集暴露为标准 *sql.Rows,后续所有遍历、扫描都由你手动控制——没有内置分页参数,也不感知页码或偏移量。
常见错误现象:
- 误以为
db.Table("users").Rows().Next()可以配合Limit(10).Offset(20)—— 实际上Limit/Offset在Rows()前已被忽略,GORM 不会将它们拼进最终 SQL - 在循环中用计数器模拟 “第 N 页”,但没加 WHERE 条件,导致每次全表扫描 + 内存过滤,性能崩盘
正确做法:用 WHERE + 排序字段代替 OFFSET
真正的流式分页必须依赖有序、唯一、可比较的字段(如自增 id、created_at),每次请求带上上一页最后一条记录的该字段值,作为下一页的起始条件。
示例场景:按 id 升序分页,每页 100 条
// 第一页:无游标
rows, err := db.Raw("SELECT id, name, email FROM users ORDER BY id ASC LIMIT ?", 100).Rows()
// 后续页:带上 last_id
lastID := 100 // 上一页最后一条的 id
rows, err := db.Raw("SELECT id, name, email FROM users WHERE id > ? ORDER BY id ASC LIMIT ?", lastID, 100).Rows()
关键点:
- 必须有
ORDER BY,且排序字段参与WHERE条件,否则无法保证顺序和去重 - 推荐用
>而非>=,避免重复数据(尤其当排序字段不唯一时) - 如果用时间戳,需注意精度(MySQL 默认秒级,可能撞值),建议组合
(created_at, id)作复合游标
Scan 如何配合 Rows 实现安全流式读取
Scan 在这里不是 GORM 的 Scan() 方法,而是 *sql.Rows.Scan() —— 它逐行将数据库列映射到 Go 变量,内存占用恒定,适合大数据量。
容易踩的坑:
- 忘记调用
rows.Close():连接不会自动释放,很快耗尽连接池 - 字段数量/类型与
Scan()参数不匹配:报错sql: expected 3 destination arguments in Scan, not 2 - 未检查
rows.Err():循环结束后应显式检查是否因 I/O 错误提前中断
基础模板:
rows, err := db.Raw("SELECT id, name, email FROM users WHERE id > ? ORDER BY id ASC LIMIT ?", lastID, 100).Rows()
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id uint
var name, email string
if err := rows.Scan(&id, &name, &email); err != nil {
return err
}
// 处理单条记录
}
if err := rows.Err(); err != nil {
return err
}
Rows + Scan 在真实高并发服务中的注意事项
这不是玩具代码,上线前必须确认三件事:
- 数据库侧是否对游标字段建了索引?
WHERE id > ? ORDER BY id必须走索引,否则还是全表扫 - 连接是否设置超时?
db.SetConnMaxLifetime()和context.WithTimeout()要配套用,防止慢查询拖垮整个连接池 - 下游是否能处理“无总条数”?游标分页天然不支持
COUNT(*),前端分页控件要改为“下一页”+“上一页”模式,而非页码跳转
最常被忽略的一点:游标值必须从本次结果中取出,不能靠客户端传入并信任——比如用户篡改 URL 中的 ?cursor=999999999,可能导致跳过大量数据。应在服务端校验该游标值是否真实存在于上一页结果中(或至少查一次存在性)。


















