Scan方法panic因自定义类型未实现指针接收的Scan方法;JSON需自定义类型统一处理nil与反序列化;time.Time精度丢失须启用parseTime=true;嵌套结构体需sqlx或手动平铺字段。

为什么 Scan 方法总 panic:nil 指针或类型不匹配
Go 的 database/sql 在扫描行时,会调用目标值的 Scan 方法。如果结构体字段是自定义类型但没实现 sql.Scanner,或实现了却接收指针时传了非指针(比如 var x MyType 然后 rows.Scan(&x) 中 x 是值而非指针),就会 panic:reflect.Set: value of type MyType is not assignable to type *MyType。
关键点在于:所有 Scan 方法签名必须是 func (s *MyType) Scan(src interface{}) error —— 接收者必须是指针,哪怕你内部不改它。因为 sql 包在反射调用时,只认可可寻址的指针接收者。
- 错误写法:
func (s MyType) Scan(...)→ 运行时报cannot call pointer method on s - 正确写法:
func (s *MyType) Scan(src interface{}) error,哪怕s是空结构体也要加* - 使用时必须传地址:
rows.Scan(&myVar),不能是rows.Scan(myVar)
JSON 字段存取:sql.NullString 不够用,得自己包一层
想把 JSON 存进 TEXT 或 JSON 列,又希望 Go 层直接映射成 map[string]interface{} 或结构体?sql.NullString 无法自动反序列化,硬塞 json.Unmarshal 到字符串上又容易漏掉 nil 处理。
最稳做法是定义一个带 Scan 和 Value 的类型,统一处理 nil、空字符串、无效 JSON:
立即学习“go语言免费学习笔记(深入)”;
type JSONB map[string]interface{}
func (j *JSONB) Scan(src interface{}) error {
if src == nil {
*j = nil
return nil
}
b, ok := src.([]byte)
if !ok {
return fmt.Errorf("cannot scan %T into JSONB", src)
}
return json.Unmarshal(b, j)
}
func (j JSONB) Value() (driver.Value, error) {
if len(j) == 0 && (*j == nil) {
return nil, nil
}
return json.Marshal(j)
}
- 注意
Scan接收src可能是[]byte(MySQL/PostgreSQL 常见)、string(SQLite)、甚至nil,别直接断言string -
Value方法返回nil表示 SQLNULL,不是空 JSON 对象{} - PostgreSQL 的
JSONB列对二进制格式敏感,但 Go 层仍按[]byte传入,不用额外 decode
时间精度丢失:time.Time 存 TIMESTAMP(6) 为什么秒后全为 0
MySQL 8.0+ 和 PostgreSQL 支持微秒级时间戳,但 Go 默认的 time.Time 在扫描时可能被截断——尤其用 mysql 驱动且未启用 parseTime=true 参数时,数据库返回的是字符串,time.Parse 默认只到秒。
解决办法不是重写 Scan,而是从连接参数和驱动行为入手:
- DSN 必须加
parseTime=true(如user:pass@tcp(127.0.0.1:3306)/db?parseTime=true) - 确保数据库列类型是
TIMESTAMP(6)或TIMESTAMPTZ,不是DATETIME(MySQL 的DATETIME不支持微秒索引) - 如果仍不准,检查驱动版本:
github.com/go-sql-driver/mysqlv1.7+ 才完整支持纳秒级解析 - 避免用
time.Now().UTC().Format(...)拼字符串再存——这会丢精度,直接传time.Time值让驱动处理
嵌套结构体怎么扫:别指望 rows.Scan 自动展开
很多人以为给结构体字段打 tag 就能自动映射嵌套字段,比如 type User struct { Profile Profile `json:"profile"` },然后 SELECT id, profile_name, profile_age FROM users —— 这不会生效。Scan 是按列顺序一对一绑定的,不解析字段名层级。
真要支持嵌套,只有两个现实路径:
- 用
sqlx.StructScan(需引入github.com/jmoiron/sqlx),它基于字段 tag 和列名做模糊匹配,支持嵌套,但要求列名能跟字段名对上(如profile_name→Profile.Name,且Profile字段得是导出的) - 手动拼
SELECT:把嵌套字段全部平铺,如SELECT u.id, p.name as profile_name, p.age as profile_age FROM users u JOIN profiles p ON u.id = p.user_id,再用普通Scan绑定到扁平结构体 - 放弃嵌套映射,用
map[string]interface{}先扫出原始数据,再手动构造嵌套结构——适合字段动态、关系复杂的情况
最常被忽略的一点:无论哪种方式,嵌套字段的 Scan 实现仍然得各自满足指针接收者 + 正确类型转换规则,不能因为“外层结构体能扫”,就默认内层也自动兼容。










