跳转到内容

go-embedding

Go Embedding:嵌入类型的字段提升、覆盖与实战用法

Section titled “Go Embedding:嵌入类型的字段提升、覆盖与实战用法”

分类: 开发 / Go 语言
记录时间: 2026-04-06
来源: yomiya-service PR #457 — SQLBoiler model 的 json:"-" 字段导致 API 响应丢失关联数据


Embedding 是把一个类型不带字段名地嵌入另一个类型。嵌入后,内层的字段和方法自动”提升”到外层,可以直接访问。

// 普通字段 — 有名字,需要通过 o.Inner.Name 访问
type Outer struct {
Inner Inner
}
// embedding — 没有字段名,o.Name 直接访问
type Outer struct {
Inner
}

type Base struct {
ID int
Name string
}
func (b Base) Hello() string { return "hello " + b.Name }
type User struct {
Base // embedding
Email string
}
u := User{Base: Base{ID: 1, Name: "Alice"}, Email: "a@b.com"}
u.Name // "Alice" — 不需要 u.Base.Name
u.Hello() // "hello Alice" — 方法也提升了
u.Base.Name // "Alice" — 显式访问也可以

2. 同名字段外层优先(Shadowing)

Section titled “2. 同名字段外层优先(Shadowing)”

编译器从最外层开始找,找到就停。外层字段完全覆盖内层同名字段,包括 struct tag。

type Inner struct {
R string `json:"-"` // 序列化时跳过
}
type Outer struct {
Inner
R string `json:"R"` // 外层覆盖,序列化时输出
}
o := Outer{Inner: Inner{R: "hidden"}, R: "visible"}
json.Marshal(o) // {"R":"visible"}

嵌入指针避免数据复制,适用于 DTO 包装场景。

type DTO struct {
*models.Episode // 零拷贝,所有字段直接提升
R *RelInfo `json:"R,omitempty"` // 覆盖内层 json:"-" 的 R
}
dto := DTO{Episode: ep}
dto.ID // 直接访问 ep.ID
dto.Title // 直接访问 ep.Title

注意:指针 embedding 时如果指针为 nil,访问提升的字段会 panic。

内层实现的接口方法提升后,外层自动满足该接口。

type Reader interface {
Read(p []byte) (n int, err error)
}
type MyReader struct{}
func (r MyReader) Read(p []byte) (int, error) { return 0, nil }
type Wrapper struct {
MyReader // Wrapper 自动满足 Reader 接口
}
var r Reader = Wrapper{} // ✅ 编译通过

两个嵌入类型有同名字段时,直接访问会编译错误,必须显式指定。

type A struct{ Name string }
type B struct{ Name string }
type C struct {
A
B
}
c := C{}
c.Name // ❌ 编译错误:ambiguous selector
c.A.Name // ✅ 显式指定

实战案例:覆盖 SQLBoiler 的 json:"-"

Section titled “实战案例:覆盖 SQLBoiler 的 json:"-"”

SQLBoiler 生成的 model 把关联数据放在 R 字段,但标记为 json:"-"

// SQLBoiler 生成 — 不可修改
type PodcastEpisode struct {
ID int64 `json:"id"`
Title null.String `json:"title,omitempty"`
// ... 其他字段 ...
R *podcastEpisodeR `json:"-"` // ← 关联数据,但 JSON 被跳过
L podcastEpisodeL `json:"-"` // ← 加载器,也被跳过
}
type podcastEpisodeR struct {
TranscriptionTask *TranscriptionTask `json:"TranscriptionTask"`
}

Controller 直接返回 model slice,API 响应里 R.TranscriptionTask 永远不出现,前端拿不到转录状态。

用 embedding + 同名覆盖,一行替代 13 个字段的手动复制:

type podcastEpisodeDTO struct {
*models.PodcastEpisode // 所有字段直接提升
R *podcastEpisodeRelationships `json:"R,omitempty"` // 覆盖 json:"-"
}
type podcastEpisodeRelationships struct {
TranscriptionTask *podcastEpisodeTaskInfo `json:"TranscriptionTask,omitempty"`
}
type podcastEpisodeTaskInfo struct {
Status string `json:"status"`
NewsID null.String `json:"news_id"`
}

序列化行为:

字段来源JSON 行为
id, title, audio_url从 model 提升正常输出
R(model 的)json:"-"被外层 R 覆盖
R(DTO 的)json:"R,omitempty"有 task 时输出
Ljson:"-"没有覆盖,继续跳过
func toPodcastEpisodeDTOs(episodes models.PodcastEpisodeSlice) []podcastEpisodeDTO {
dtos := make([]podcastEpisodeDTO, 0, len(episodes))
for _, ep := range episodes {
dto := podcastEpisodeDTO{PodcastEpisode: ep}
if task := ep.GetTranscriptionTask(); task != nil {
dto.R = &podcastEpisodeRelationships{
TranscriptionTask: &podcastEpisodeTaskInfo{
Status: task.Status,
NewsID: task.NewsID,
},
}
}
dtos = append(dtos, dto)
}
return dtos
}

场景做法
包装生成代码的 model,补充/覆盖 JSON tag指针 embedding + 同名字段覆盖
给 struct 添加接口实现而不修改原类型embedding 原类型,外层添加方法
组合多个能力(类似 mixin)多个 embedding
需要完全控制序列化格式不用 embedding,手动定义字段