go-embedding
Go Embedding:嵌入类型的字段提升、覆盖与实战用法
Section titled “Go Embedding:嵌入类型的字段提升、覆盖与实战用法”分类: 开发 / Go 语言
记录时间: 2026-04-06
来源: yomiya-service PR #457 — SQLBoiler model 的 json:"-" 字段导致 API 响应丢失关联数据
什么是 Embedding
Section titled “什么是 Embedding”Embedding 是把一个类型不带字段名地嵌入另一个类型。嵌入后,内层的字段和方法自动”提升”到外层,可以直接访问。
// 普通字段 — 有名字,需要通过 o.Inner.Name 访问type Outer struct { Inner Inner}
// embedding — 没有字段名,o.Name 直接访问type Outer struct { Inner}1. 字段和方法提升(Promotion)
Section titled “1. 字段和方法提升(Promotion)”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.Nameu.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"}3. 指针 Embedding
Section titled “3. 指针 Embedding”嵌入指针避免数据复制,适用于 DTO 包装场景。
type DTO struct { *models.Episode // 零拷贝,所有字段直接提升 R *RelInfo `json:"R,omitempty"` // 覆盖内层 json:"-" 的 R}
dto := DTO{Episode: ep}dto.ID // 直接访问 ep.IDdto.Title // 直接访问 ep.Title注意:指针 embedding 时如果指针为 nil,访问提升的字段会 panic。
4. 接口自动满足
Section titled “4. 接口自动满足”内层实现的接口方法提升后,外层自动满足该接口。
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{} // ✅ 编译通过5. 多个 Embedding 的歧义
Section titled “5. 多个 Embedding 的歧义”两个嵌入类型有同名字段时,直接访问会编译错误,必须显式指定。
type A struct{ Name string }type B struct{ Name string }
type C struct { A B}
c := C{}c.Name // ❌ 编译错误:ambiguous selectorc.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 时输出 |
L | json:"-" | 没有覆盖,继续跳过 |
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}适用场景总结
Section titled “适用场景总结”| 场景 | 做法 |
|---|---|
| 包装生成代码的 model,补充/覆盖 JSON tag | 指针 embedding + 同名字段覆盖 |
| 给 struct 添加接口实现而不修改原类型 | embedding 原类型,外层添加方法 |
| 组合多个能力(类似 mixin) | 多个 embedding |
| 需要完全控制序列化格式 | 不用 embedding,手动定义字段 |