scan不能直接映射结构体,必须传字段地址切片或使用&u;字段名需严格匹配(或通过db/gorm标签指定),且结构体字段必须导出、null值须用sql.null类型或指针接收,否则会panic或数据错位。

Scan 能直接映射到结构体,但必须传指针地址、字段名需匹配、NULL 值要显式处理——否则会 panic 或数据错位。
Scan 映射结构体时为什么字段值为空或错乱
常见错误是把结构体变量本身传给 Scan,比如写成 db.Raw("SELECT name FROM users").Scan(user)。这会导致编译通过但运行时 panic:无法对非指针类型调用 Scan。正确做法是传地址:&user 或 &users(切片)。
字段名不匹配也会导致赋值失败:GORM 默认按 SQL 返回列名(不区分大小写)匹配结构体字段名,若列名是 user_name 而结构体字段叫 UserName,且没加 gorm:"column:user_name" 标签,该字段就收不到值。
- 结构体字段必须导出(首字母大写)
- SQL 列名和结构体字段名默认按字符串全等匹配(
"name"↔Name),不自动做驼峰/下划线转换 - 如果用了 JOIN 且多表有同名列(如
users.id和orders.id),必须用别名,否则 GORM 无法区分
Scan 接收单个结构体 vs 切片的写法差异
Scan 对单个结构体和切片的处理逻辑一致,但调用方式不同:前者传结构体指针,后者传切片指针。
- 查一行 → 用
var user User; db.Raw("SELECT * FROM users LIMIT 1").Scan(&user) - 查多行 → 用
var users []User; db.Raw("SELECT * FROM users").Scan(&users)(注意是&users,不是users) - 查单个字段 →
var name string; db.Raw("SELECT name FROM users WHERE id = ?", 1).Scan(&name) - 查聚合值 →
var count int64; db.Raw("SELECT COUNT(*) FROM users").Scan(&count)
所有情况都要求 SQL 返回列数与目标变量字段数严格一致;多列查询却只传一个 &name,就会报 sql: expected 1 destination arguments, got X。
NULL 值怎么安全接收
数据库字段允许 NULL 时,不能直接用 string、int 等基础类型接收,否则 Scan 会报 sql: Scan error on column index X。
- 用
sql.NullString、sql.NullInt64等标准类型,它们自带Valid字段标识是否为 NULL - 结构体中对应字段声明为
Name sql.NullString,然后在 tag 中补上gorm:"column:name"(确保列名对得上) - 不想改结构体?可用
coalesce(name, '')在 SQL 层兜底,但仅限确定可默认填充的场景
注意:sql.NullXXX 类型不能直接参与 JSON 序列化,需要额外实现 MarshalJSON 方法,或者用第三方库如 guregu/null。
Scan 和 Find 的关键区别在哪
Find 是 GORM 封装好的“全自动”查询,会走完整 ORM 流程(包括预处理、关联加载、钩子触发);Scan 是“半手动”结果绑定,绕过大部分 ORM 逻辑,只负责把 *sql.Rows 填进目标变量。
-
Find(&users):自动用模型定义的表名、字段映射、软删除条件等,适合标准 CRUD -
Scan(&users):适合原生 SQL、复杂 JOIN、聚合查询、或想跳过 GORM 缓存/钩子的场景 -
Scan不会触发AfterFind钩子,也不受Select()字段白名单影响(它只看 SQL 实际返回什么)
真正容易被忽略的是:Scan 不校验字段类型兼容性——比如把 datetime 列扫进 int 字段,可能静默失败或值异常,调试时得靠日志或手动检查 rows.Columns()。











