必须同时实现sql.Scanner和driver.Valuer接口,缺一则GORM读写任一环节panic;因database/sql仅支持基础类型,自定义类型需自行定义序列化(Value)和反序列化(Scan)逻辑。

必须同时实现 sql.Scanner 和 driver.Valuer,缺一不可;否则 GORM 在读或写任意一端都会 panic。
为什么 Scan 和 Value 必须成对出现
database/sql 不认识你的自定义类型,它只接受基础类型(string、[]byte、int64 等)。当你把一个结构体或切片直接塞进 db.QueryRow().Scan() 或传给 db.Exec(),就会报错:sql: converting driver.Value type XXX to a <nil> is not supported。
解决路径只有一条:让类型自己说清「我怎么存」「我怎么读」。
-
Value()负责写入前把自定义值转成数据库能收的格式(通常是string或[]byte) -
Scan()负责读取后把数据库返回的原始值(比如[]byte)还原成你的类型 - GORM(v2+)在迁移时默认跳过未显式声明
type:的字段,即使你实现了两个接口,没写 tag 也会建出bytea或blob,跟你的序列化逻辑不匹配
如何正确实现 AvatarListJSON 这类切片类型
以存储用户已解锁头像 ID 列表([]int)为例,常见错误是只实现 Value()、漏掉 Scan(),或者在 Scan() 里没处理 nil 或空字节流。
立即学习“go语言免费学习笔记(深入)”;
正确写法要覆盖边界情况:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
Scan()入参value可能是nil、[]byte、string,必须做类型断言和长度判断 -
Value()返回前建议检查零值,避免传nil给json.Marshal导致 panic - 字段 tag 中必须显式写
gorm:"type:text"(MySQL)或gorm:"type:jsonb"(PostgreSQL),不能依赖serializer:json—— 后者会绕过你的Scan/Value逻辑
示例片段:
type AvatarListJSON []int
func (al *AvatarListJSON) Scan(value interface{}) error {
if value == nil {
*al = []int{}
return nil
}
bytes, ok := value.([]byte)
if !ok {
return fmt.Errorf("cannot scan %T into AvatarListJSON", value)
}
if len(bytes) == 0 {
*al = []int{}
return nil
}
var temp []int
if err := json.Unmarshal(bytes, &temp); err != nil {
return err
}
*al = AvatarListJSON(temp)
return nil
}
func (al AvatarListJSON) Value() (driver.Value, error) {
if len(al) == 0 {
return []byte("[]"), nil
}
return json.Marshal(al)
}
GORM v2+ 中 serializer:json 和自定义 Scan/Value 的冲突点
很多人图省事,在字段上加 gorm:"serializer:json",以为就能自动处理结构体或切片。但这个机制和手写的 Scan/Value 是互斥的:
- 一旦用了
serializer:json,GORM 就不再调用你实现的Scan/Value方法 - 它会用自己的 JSON 编解码器,且不支持自定义行为(比如加密、字段过滤、兼容旧数据格式)
- 如果你的类型需要特殊反序列化逻辑(例如兼容字符串型数字 ID),
serializer:json会直接失败
所以真实项目中,只要逻辑稍复杂(如字段名映射不一致、需 fallback 处理、含 time.Time 子字段),就该关掉 serializer,老老实实写 Scan/Value,并在 tag 中明确指定 type: 和是否 not null。
容易被忽略的底层细节
最常踩的坑不在逻辑,而在内存和类型安全:
-
Scan()接收的[]byte是数据库驱动临时分配的,不能直接保存指针,必须拷贝或反序列化到新变量 -
Value()返回string没问题,但返回[]byte时要注意:某些驱动(如 pq)要求不可变,改了可能引发并发读写冲突 - 如果类型嵌套了
time.Time或指针字段,json.Unmarshal默认不会初始化 nil 指针,得靠json.RawMessage或自定义 UnmarshalJSON 配合处理
这些不是“写了就能跑”,而是“写了之后某天线上突然 panic”的那种点。

















