分页应优先用find,因其自动映射结构体并复用链式条件;scan适用于字段级控制、游标分页或流式处理大数据,但需手动管理字段对齐与连接生命周期。

分页时用 Find 还是 Scan,不是风格问题,而是语义和资源控制的硬差别——Find 自动映射结构体并复用链式条件,Scan 则绕过模型绑定、直取原始列,且不继承 Limit/Offset 以外的链式上下文。
Find 分页:结构体绑定 + 隐式模型推导
Find 是 GORM 分页最常用的方式,但它依赖明确的结构体目标和隐含的模型信息。一旦你传入一个空切片或未注册的结构体,GORM 就可能无法推断表名或字段映射关系。
- 必须传入已定义的 struct 类型切片(如
var users []User),否则会报invalid destination address或failed to find primary key field - 若使用 map 接收(如
var results []map[string]interface{}),必须显式调用Table("users"),否则 GORM 不知道查哪张表 -
Find会完整复用前面所有链式调用(Where、Order、Joins),但Count会意外带上Limit/Offset——这是高频翻车点,总数得另起新 Session 或手写子查询 - 不支持只查部分字段后直接绑到结构体(除非结构体字段带
gorm标签且数据库列名匹配),否则缺失字段会被零值填充
Scan 分页:字段级控制 + 脱离模型约束
Scan 不走模型反射流程,它只关心 SQL 返回的列顺序和类型匹配,因此更轻量、更可控,但也更“裸”——你得自己管字段对齐、类型转换、甚至连接生命周期。
- 可直接扫到基础类型变量(
var name string; db.Select("name").Where("id = ?", 1).Scan(&name)),也可扫到 struct、map、[]interface{},但字段顺序必须和Select一致 - 不会自动应用
Preload或关联嵌套逻辑;涉及Joins时,字段名冲突(如两个表都有id)会导致扫描失败或静默错位,必须显式别名(SELECT u.id AS uid, p.name) -
Scan不继承Limit/Offset吗?错——它会继承,但只继承显式写的;如果用Raw拼 SQL,Scan就完全不管链式状态,纯靠 SQL 字符串本身 - 大数据量分页时,
Scan配合Rows可流式处理(rows.Next()+rows.Scan()),避免一次性加载全量内存,而Find总是一次性分配切片并填满
分页总数统计:Find 和 Scan 都不能直接复用分页链
无论你用 Find 还是 Scan 做分页查询,都**不能**在同一个 db 实例上调用 Count 来获取总数——Count 会把前面的 Limit 和 Offset 也带上,结果永远 ≤ 每页条数。
- 安全做法是新建独立 Session:
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total) - 复杂 Joins 场景下,
Count容易因笛卡尔积虚高,此时必须手写子查询:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE ...) t").Scan(&total) - 如果业务允许弱一致性(比如总数仅作估算),可缓存总数或用近似值(MySQL 的
EXPLAIN行数),避免每次分页都查一遍
游标分页场景下,Scan 更自然,Find 很难适配
当数据量上百万、OFFSET 开始拖慢响应时,游标分页(基于 WHERE id > ? ORDER BY id)是唯一靠谱方案。这时 Scan 的灵活性明显优于 Find。
-
FindInBatches内部其实也是基于主键游标 +Scan逻辑实现的,它不走Find的完整模型绑定路径,而是手动构造 WHERE 条件并逐批扫 - 如果你要自定义游标字段(比如按
created_at, id复合排序),Scan可以轻松接收两个参数:var createdAt time.Time; var id uint; rows.Scan(&createdAt, &id);而Find必须定义对应 struct 字段,且需确保Order和Where严格匹配 - 游标值本身(如上一页最后一条的
id)通常来自前一次Scan结果,而不是Find返回的 struct——因为你要的只是游标,不是整行数据
真正容易被忽略的是:分页的稳定性不取决于用 Find 还是 Scan,而在于是否强制加了确定性 Order,以及总数查询是否脱离了分页上下文。这两点错了,换什么方法都救不回漏数据或总数错乱。











