Find自动映射但有零值过滤和反射开销,Scan更轻量高效但需手动维护SQL与结构体一致性。

Find 会自动映射,但带来额外字段校验和零值过滤开销
Find 方法在执行前会对传入的 struct 实例做字段值检查:int 类型为 0、string 为 ""、bool 为 false 等零值字段,默认被跳过 WHERE 条件。这看似省事,实则隐含两层成本:一是反射遍历结构体字段判断是否零值;二是生成 SQL 时动态拼接条件逻辑。尤其当结构体嵌套深、字段多时,这部分开销不可忽略。
更关键的是,这种“智能”常导致查询语义错位——比如你想查 Status = 0 的记录,db.Where(&user).Find(&users) 实际发出去的 SQL 根本不含该条件,结果为空却无报错。这不是 bug,是设计选择,但代价是调试成本上升。
Scan 不走模型映射路径,原生 SQL + 显式绑定更轻量
Scan 绕过了 GORM 的 struct-to-table 字段映射机制,也不触发 AfterFind 钩子、不处理关联预加载、不校验零值。它只做一件事:把 SQL 查询结果按顺序或按别名,直接拷贝进你提供的变量或结构体字段。
这意味着:
- SQL 由你完全控制,可精准裁剪字段(如
SELECT id, name),避免冗余数据传输 - 绑定目标可以是任意类型:
string、int、匿名结构体、甚至map[string]interface{} - 没有反射字段扫描,纯指针拷贝,基准测试中通常比
Find快 15%–30%
典型用法:db.Raw("SELECT COUNT(*) FROM users WHERE status = ?").Scan(&count),这里没模型、没零值干扰、没钩子调用,就是一条 SQL 到一个整数。
联表查询时 Scan 是唯一可靠选择
用 Joins 做左外连接时,如果还用 Find,GORM 会尝试把结果映射回主模型(如 Order),而关联表字段(如 users.name)因无对应 struct 字段被丢弃,或填零值。最终你拿到的是一堆 Order 实例,但 User 相关字段全为空——看起来像左连接失败,其实是映射能力不足。
正确做法是显式 Select 字段 + Scan 到能容纳全部列的目标结构体:
type OrderWithUser struct {
OrderID uint
OrderNo string
UserName string `gorm:"column:name"`
}
var results []OrderWithUser
db.Table("orders").
Select("orders.id as order_id, orders.no as order_no, users.name").
Joins("LEFT JOIN users ON users.id = orders.user_id").
Scan(&results)
注意字段名必须与 SQL 中的列名或别名严格一致(支持下划线转驼峰,但别名优先),且目标必须是指针切片。
Scan 的隐藏代价:你要自己管好 SQL 和结构体一致性
Scan 快,但快的前提是你对 SQL 和接收结构体有完全掌控力。一旦 SQL 改了列名、加了新字段、调整了顺序,而结构体没同步更新,就会静默出错:字段错位、类型不匹配、甚至 panic。
而 Find 虽慢一点,但靠 struct tag(如 gorm:"column:user_name")和模型定义兜底,SQL 变化影响小,适合字段稳定、开发迭代快的业务场景。
真正容易被忽略的点是:Scan 不释放 *sql.Rows 资源——它内部封装了自动关闭,但如果你混用 Rows() + Scan 手动遍历,就可能漏掉 rows.Close(),导致连接池耗尽。这点在压测时才容易暴露。


















