必须实现 driver.Valuer 和 sql.Scanner 接口,GORM 才能识别自定义类型并完成数据库读写;否则会报错或静默丢弃字段,因其仅默认支持基础类型和部分内置映射,依赖这两个接口完成 SQL 值与 Go 值的双向转换。

必须实现 driver.Valuer 和 sql.Scanner 两个接口,GORM 才能识别你的自定义类型并完成数据库读写。不实现任一接口,GORM 会报错或静默丢弃字段。
为什么 GORM 不直接支持自定义结构体存库
GORM 默认只处理基础类型(int、string、time.Time 等)和部分内置映射(如 json.RawMessage)。遇到自定义 struct 或 slice,它既不知道怎么转成 SQL 值,也不知道怎么把数据库返回的 []byte 或 string 反解回 Go 值。
底层驱动(如 pgx 或 mysql)只认 driver.Valuer.Value() 输出的值,以及 sql.Scanner.Scan() 能接收的输入格式。GORM 在中间不做额外转换,全靠你补上这两端契约。
-
Value()方法决定「存什么」:返回driver.Value(通常是string、[]byte或基本类型) -
Scan()方法决定「读成啥」:接收interface{},内部做类型断言 + 解析,赋值给接收者指针 - 两个方法必须配对;漏掉
Scan,查出来字段是零值;漏掉Value,插入时报unsupported type
PostgreSQL POINT 类型的典型实现
以地理点 Point 为例:PostgreSQL 原生 POINT 存的是 (x,y) 字符串,而 Go 里你想要一个干净的 struct。
type Point struct {
X, Y float64
}
func (p Point) Value() (driver.Value, error) {
return fmt.Sprintf("(%.6f,%.6f)", p.X, p.Y), nil
}
func (p *Point) Scan(value interface{}) error {
s, ok := value.(string)
if !ok {
return fmt.Errorf("cannot scan %T into Point", value)
}
// 解析 "(123.45,67.89)" → 提取两个浮点数
if len(s)
- 注意
Scan接收者是指针*Point,否则无法修改原值 -
Value返回string是因为 PostgreSQL 的POINT输入函数接受字符串字面量 - 解析时做基础校验(括号包裹、逗号分隔),避免运行时 panic
JSON 字段更简单的替代方案:serializer 标签
如果你只是存 JSON(比如 map[string]interface{} 或结构体),不必手写 Valuer/Scanner —— GORM v1.25+ 支持 serializer 标签:
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Info Info `gorm:"serializer:json"`
}
type Info struct {
Status string `json:"status"`
Addr string `json:"addr"`
}
- 该方式自动调用
json.Marshal/json.Unmarshal,无需手动实现接口 - 但仅适用于 JSON;POINT、数组、自定义二进制协议等仍需手写接口
- 注意:字段类型必须是可 JSON 序列化的,且不能是别名 slice(如
type Strs []string),否则会失败
最容易被忽略的是 Scan 方法里的类型判断逻辑——数据库返回的 value 在不同驱动下可能是 []byte(MySQL)、string(PostgreSQL pgx)甚至 nil,硬写 s := value.(string) 在某些环境会 panic。务必先做 switch v := value.(type) 或统一转成 []byte 再处理。











