必须同时实现 sql.scanner 和 driver.valuer 接口,因 gorm 读写分离:value() 负责序列化写入(如转 []byte),scan() 负责反序列化读取;缺一则导致单向失败,运行时 panic 或 automigrate 异常。

必须实现 Scanner 和 Valuer 两个接口,缺一不可;否则 AutoMigrate 会失败,或运行时 panic 报 unsupported type。
为什么 GORM 要求同时实现 Scan 和 Value?
GORM 在读写数据库时是分离的:写入走 Value() 方法序列化为数据库可存类型(如 []byte 或 string),读取走 Scan() 方法反序列化回 Go 值。只实现一个会导致单向失联——比如能存不能查,或能查不能存。
常见错误现象:
-
panic: unsupported driver.Value type struct { ... } in column X→ 没实现Value -
sql: Scan error on column X: unsupported Scan, storing driver.Value type []uint8 into type *main.MyType→ 没实现Scan -
AutoMigrate成功但插入时报错 → 类型没被识别,GORM 当作普通 struct 处理,未触发自定义逻辑
Scan 和 Value 的参数与返回值怎么写才安全?
这两个方法签名是硬性约定,不能改名、不能改参数数量或顺序,否则 GORM 不会调用它们。
Scan 必须接收 interface{} 并做类型断言,最常见的是 []byte;Value 必须返回 driver.Value(即 any)和 error:
func (j *Info) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New("cannot scan non-[]byte into Info")
}
return json.Unmarshal(bytes, j)
}
func (j Info) Value() (driver.Value, error) {
return json.Marshal(j)
}
注意点:
-
Scan的接收者必须是指针(*Info),否则无法修改原值 -
Value的接收者必须是值类型(Info),避免指针引发 nil panic - 空值处理要显式判断:
if len(bytes) == 0时应设默认值,否则json.Unmarshal(nil, &j)会静默失败
如何让 GORM 正确识别并建表?
仅实现接口还不够,GORM 需要明确知道字段该映射成什么数据库类型,否则 AutoMigrate 可能建出 TEXT 或直接报错。
必须在结构体 tag 中显式指定 type:,或实现 GormDataType() 方法:
- 用 tag 最简单:
Info Info `gorm:"type:text"`(MySQL)或gorm:"type:jsonb"(PostgreSQL) - 若想跨数据库兼容,推荐实现
GormDataType()方法,例如:func (Info) GormDataType() string { return "json" } - 不指定时,GORM 默认按底层 Go 类型推断(如 struct →
text),但不保证语义正确,尤其对 JSON 字段
另外,字段不能是匿名嵌入(如 Info 直接嵌入 User),否则 tag 不生效;必须显式声明字段名 + tag。
JSON 切片/数组类型最容易踩的坑
用 []string 或 []int 这类切片自定义类型时,Scan 和 Value 的实现细节极易出错:
-
Scan中误用value.(string)→ 实际从 DB 读出的是[]byte,强转 string 再 json.Unmarshal 会多一层编码 -
Value返回string(jsonBytes)→ 错!应直接返回jsonBytes([]byte),否则 MySQL 会当字符串存,带引号,后续反序列化失败 - 没处理空切片:
[]string{}经json.Marshal得[],但某些驱动可能传nil进Scan,需判空
更稳妥的做法是统一用 []byte 作为中间载体,避免 string/[]byte 混用导致的隐形编码问题。
真正麻烦的不是写这两个方法,而是确保它们在所有边界场景下行为一致:空值、null、损坏 JSON、不同数据库驱动返回的原始类型差异——这些地方一漏,线上就静默丢数据。











