scan()要求结构体字段与select列严格一一对应,包括顺序、名称、类型;否则静默填零或panic;聚合查询应直接scan到基础类型,避免冗余结构体。

必须先定义结构体,且字段与 SELECT 列严格对齐,否则 Scan() 会静默填零或 panic。
结构体字段必须和 SELECT 列一一对应
Scan() 不做字段名映射、不自动转换类型、不忽略多余列。SELECT name, age, created_at FROM users 返回三列,结构体就必须恰好三个字段,顺序、名称、类型完全一致。
- 字段名不匹配(比如 SQL 中写
name AS username,但结构体字段叫Name且没配gorm:"column:username")→ 值为零值 - 类型不匹配(比如 SQL 返回
INT,结构体字段是string)→panic: cannot scan into dest - 多查一列或少查一列 → 同样 panic,错误信息明确提示“expected X columns, got Y”
单行 vs 多行:传参必须带取地址符 &
查单行用 db.Raw(sql).Scan(&u),查多行用 db.Raw(sql).Scan(&users) —— 注意都是 &,不是 u 或 users。
-
var u User; db.Raw("SELECT ...").Scan(&u)→ 正确,填充单个结构体 -
var users []User; db.Raw("SELECT ...").Scan(&users)→ 正确,填充切片 -
db.Raw("...").Scan(users)→ 编译通过但运行时 panic:“cannot scan into nil pointer”
聚合/标量结果别硬塞结构体
像 COUNT(*)、SUM(price)、MAX(id) 这类返回单个基础值的查询,直接 Scan 到 int64、float64 等原生类型最稳妥。
- ✅ 正确:
var count int64; db.Raw("SELECT COUNT(*) FROM users").Scan(&count) - ❌ 错误:
type Count struct{ Total int64 }; var c Count; db.Raw("...").Scan(&c)—— 多余且易因 tag 或命名出错 - 更轻量的选择:
db.Raw("SELECT COUNT(*)").Row().Scan(&count),避免构造中间结构体
MySQL 不支持 RETURNING,UPDATE 后取新值要分两步
PostgreSQL 可用 UPDATE ... RETURNING id, name + Scan(&u) 一次完成;MySQL 没这语法,强行套用会报错 ERROR 1064。
- 正确做法:先
db.Exec("UPDATE ..."),再单独db.Raw("SELECT ... WHERE id = ?").Scan(&u) - 注意事务包裹:两步操作需在同一个
tx := db.Begin()内,否则可能读到旧值 - 如果只想要新插入 ID,优先用
db.Create(&u).Error后取u.ID,比手写INSERT ...; SELECT LAST_INSERT_ID()更可靠
最容易被忽略的是字段顺序和空值处理:即使类型匹配,SQL 中某列为 NULL 而结构体字段是非指针类型(如 int),Scan 会失败;此时必须改用 *int 或 sql.NullInt64。











