scan必须传指针而非值,因go是值传递,需通过指针获取变量地址写入数据;传值会导致修改副本而原变量不变,且类型不匹配会编译报错或panic。

为什么 Scan 必须传指针,不能传值?
因为 Scan 要把数据库查出来的数据“写回”你的变量,而 Go 的函数参数是值传递——传普通变量进去,Scan 内部改的是副本,原变量毫无变化。
这和 json.Unmarshal、fmt.Scanf 是同一套逻辑:需要可寻址的内存位置才能写入。
- 传
int会编译报错:cannot use x (type int) as type *int in argument to rows.Scan - 传
&x才合法,Scan通过指针拿到x的地址,把解析后的整数存进去 - 哪怕类型是 struct 或自定义类型,只要字段可导出且能被赋值,也必须传
&v,不是v
Scan 对 nil 指针和空结构体的处理差异
传 nil 指针进去会 panic,但传一个字段全为零值的 struct 指针不会——Scan 只负责往已有内存里填值,不负责分配内存。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
var u User; rows.Scan(&u)✅ 正常,u已分配栈空间 -
var u *User; rows.Scan(u)❌ panic:panic: sql: Scan error on column index 0: destination not a pointer -
var u *User = nil; rows.Scan(&u)❌ 还是 panic,因为&u是**User,类型不匹配 - 想支持 NULL 字段?用
sql.NullString等包装类型,它们本身带Valid bool字段,且是值类型,传&nullStr即可
常见类型匹配陷阱:字符串、时间、JSON 字段怎么扫才不丢数据
数据库字段类型和 Go 变量类型不严格对应时,Scan 会尝试转换,但失败就直接报错;成功也可能隐式截断或丢失精度。
- MySQL
TEXT或长VARCHAR→ Gostring:没问题,Scan自动分配内存 - PostgreSQL
JSONB→ Gomap[string]interface{}:会 panic,得用json.RawMessage或自定义 struct +&v - MySQL
DATETIME→ Gotime.Time:必须确保sql.Open的 DSN 包含parseTime=true,否则扫出来是字符串 - 整数列含
NULL→ 不能用*int(Go 没有 nullable 基础类型),得用sql.NullInt64并传&nullInt
性能提示:别在循环里反复声明变量再取地址
每次调用 Scan 都要传变量地址,如果在 for 循环里每次都 new 一个 struct 或重声明变量,不仅多分配,还可能触发 GC 压力。
- ✅ 推荐:在循环外声明变量,循环内复用地址:
var u User; for rows.Next() { rows.Scan(&u); ... } - ❌ 避免:
for rows.Next() { var u User; rows.Scan(&u); ... }—— 每次都新占栈,且&u地址不同,无法利用 CPU 缓存局部性 - 如果扫描结果要长期持有,记得深拷贝,因为复用变量意味着下一次
Scan会覆盖上一次的值
最易忽略的一点:即使你用了正确的指针,如果 rows.Columns() 返回的字段数和 Scan 提供的参数个数不一致,panic 发生在运行时,且错误信息只说 number of columns in result set does not match number of variables,不会告诉你哪一行哪一列对不上——建议用 struct + Scan 配合 database/sql 的列名映射逻辑,或至少在开发期加个 len(cols) == len(args) 断言。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










