
本文介绍如何通过 go 接口抽象与泛型无关的类型约束,为所有嵌入公共基础结构体(如 user)的模型提供统一的 json 序列化与数据库存取逻辑,避免重复编写 save 方法。
本文介绍如何通过 go 接口抽象与泛型无关的类型约束,为所有嵌入公共基础结构体(如 user)的模型提供统一的 json 序列化与数据库存取逻辑,避免重复编写 save 方法。
在 Gin 构建的 MVC 风格 Web 应用中,常见做法是定义多个业务模型(如 User、Admin、Editor),它们均通过匿名字段嵌入一个共享基础结构体(如 User)。此时若需将整个模型以 JSON 形式持久化到数据库(例如 PostgreSQL 的 JSONB 字段),理想方案是只编写一份 Save 逻辑,而非为每个模型复制粘贴几乎相同的代码。
直接使用 interface{} 参数是最轻量的起点:
func Save(i interface{}, id int) error {
data, err := json.Marshal(i)
if err != nil {
return fmt.Errorf("marshal failed: %w", err)
}
if id == 0 {
_, err = app.DB.Exec(`INSERT INTO users(data) VALUES ($1)`, string(data))
} else {
_, err = app.DB.Exec(`UPDATE users SET data = $1 WHERE id = $2`, string(data), id)
}
return err
}调用时传入具体实例及其 ID 即可:
u := User{ID: 1, Name: "zhora"}
a := Admin{User: u, Level: 2}
Save(u, u.ID) // → {"ID":1,"Name":"zhora"}
Save(a, a.ID) // → {"ID":1,"Name":"zhora","Level":2}但此方式缺乏编译期类型安全和语义表达力。更推荐的做法是定义一个轻量接口 Model,明确约定“可持久化的实体必须提供 ID 访问能力”:
type Model interface {
ID() int
}注意:根据 Effective Go 的规范,Getter 方法应使用首字母大写的导出名(如 ID()),而非 GetID();且无需显式 setter —— ID 通常由数据库生成或业务逻辑赋值,直接操作字段即可。
各模型只需实现该接口:
使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
func (u User) ID() int { return u.ID }
func (a Admin) ID() int {
return a.User.ID // 委托给嵌入字段
}✅ 关键点:Go 的嵌入机制天然支持方法委托。只要 Admin 嵌入 User,且 User 实现了 ID(),Admin 就自动满足 Model 接口(除非显式覆盖)。
最终 Save 函数升级为类型安全版本:
func Save(m Model) error {
data, err := json.Marshal(m)
if err != nil {
return fmt.Errorf("json marshal failed: %w", err)
}
id := m.ID()
if id == 0 {
_, err = app.DB.Exec(`INSERT INTO users(data) VALUES ($1)`, string(data))
} else {
_, err = app.DB.Exec(`UPDATE users SET data = $1 WHERE id = $2`, string(data), id)
}
return err
}调用简洁清晰:
Save(User{ID: 1, Name: "zhora"}) // INSERT
Save(Admin{User{ID: 2}, Level: 5}) // UPDATE (ID=2)⚠️ 注意事项:
- 所有需 Save 的结构体必须导出字段(首字母大写),否则 json.Marshal 会忽略私有字段;
- 若基础结构体含指针字段(如 *string)或自定义 MarshalJSON 方法,需确保其行为符合预期;
- 实际项目中建议将 app.DB 抽象为依赖参数(如 Save(db *sql.DB, m Model)),提升可测试性;
- 后续若需扩展(如软删除标记、时间戳自动填充),可在 Model 接口中增加 CreatedAt()、UpdatedAt() 等方法,保持统一生命周期管理。
这一设计兼顾简洁性、可维护性与 Go 的惯用风格,是处理多态模型持久化的典型实践。

















