
本文介绍如何让 gorm 将 go 嵌入结构体(如地理坐标)序列化为单字段(如 json),而非创建独立关联表,通过自定义 `scan` 和 `value` 方法实现透明的 json 编解码。
在使用 GORM 时,若直接将结构体(如 GeoPoint)作为字段嵌入模型(如 A),GORM 默认会将其识别为关联关系,并尝试创建外键或新表——这与期望的「扁平化存储为单个 JSON 字段」相悖。解决该问题的核心思路是:让 GORM 将该字段视为普通数据库列(如 TEXT 或 JSON 类型),并通过实现 driver.Valuer 和 sql.Scanner 接口,自动完成结构体 ↔ JSON 字符串的双向转换。
以下是一个完整、可运行的实践示例,以地理坐标 GeoPoint 为例:
import (
"database/sql/driver"
"encoding/json"
"gorm.io/gorm"
)
type GeoPoint struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
// 实现 sql.Scanner 接口:从数据库读取时反序列化 JSON
func (p *GeoPoint) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return fmt.Errorf("cannot scan %T into GeoPoint", value)
}
return json.Unmarshal(b, p)
}
// 实现 driver.Valuer 接口:写入数据库前序列化为 JSON 字符串
func (p GeoPoint) Value() (driver.Value, error) {
return json.Marshal(p)
}
// 模型定义:嵌入 GeoPoint 作为普通字段(非关联)
type A struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100"`
Point GeoPoint `gorm:"column:point;type:json"` // PostgreSQL 推荐用 type:json;MySQL 8.0+ 同样支持;旧版 MySQL 可用 type:longtext
}✅ 关键要点说明:
- gorm:"column:point;type:json" 显式指定列名与数据库类型,避免 GORM 自动建表;
- type:json 在 PostgreSQL / MySQL 8.0+ 中能获得原生 JSON 支持(含索引、查询能力);若使用旧版 MySQL,可改为 type:longtext 并确保 json.Marshal/Unmarshal 正常工作;
- Scan 和 Value 方法必须分别接收 *GeoPoint(指针)和 GeoPoint(值)——这是接口签名要求,不可交换;
- 若结构体需为空值安全(如允许 NULL),Scan 中应先判断 value == nil 并置零结构体。
⚠️ 注意事项:
- 不要为 GeoPoint 添加 gorm.Model 或 gorm.BaseModel 字段,否则 GORM 仍可能误判为嵌入模型;
- 避免在 GeoPoint 中定义 gorm 标签(如 gorm:"primaryKey"),否则干扰序列化逻辑;
- 若使用 SQLite,type:json 不被原生支持,建议统一用 type:text + 手动 JSON 处理。
通过该方式,A 表仅包含 id, name, point 三列,其中 point 存储形如 {"lat":39.9042,"lon":116.4074} 的 JSON 字符串,完全满足嵌入结构体扁平化存储的需求,同时保持代码简洁与数据一致性。










