bigywater.tech
Go · 26

结构体嵌入:方法提升、遮蔽与接口嵌入

Go 没有继承,嵌入是“组合 + 自动转发”。这篇用真实输出看清它到底做了什么:提升的字段与方法、遮蔽、嵌入指针与嵌入值的方法集、嵌入接口做装饰器、与 encoding/json 的交互,以及容易误以为有的“虚函数”其实没有。

没有虚函数
Animal.Intro 里调用的是 Animal.Speak,而不是外层 Dog.Speak
T / *T
嵌入 T 时 Dog 的方法集没有 Rename,嵌入 *T 时 Cat 就有
13 字节
countWriter 只改写了 Write,统计到 "hello, world!" 的字节数

提升与遮蔽

实测 · 本机执行输出
  d.Name = "旺财"(提升的字段);d.Animal.Name = "旺财";d.Rename("小黄") 之后 d.Name = "小黄"
  Dog 自己定义了 Speak:d.Speak() = "小黄 汪汪";d.Animal.Speak() = "小黄 发出声音"
  d.Intro()(Animal 的方法,里面调用 a.Speak()):"我是 小黄,我的声音:小黄 发出声音" ——不会调用 Dog 的 Speak(没有虚函数分派)

嵌入字段的字段与方法会被提升到外层,可以直接 d.Name、d.Rename(...)。外层定义同名方法时遮蔽被嵌入的那个,但嵌入的方法并没有消失,可以通过 d.Animal.Speak() 访问。更深的层级里同名会被更浅的遮蔽;同一深度的同名字段是歧义,编译器拒绝(第五节)。

不是继承

实测 · 本机执行输出
  Animal 的方法集:Intro,Speak;*Animal 的方法集:Intro,Rename,Speak
  Dog(嵌入 Animal)的方法集:Intro,Speak;*Dog 的方法集:Intro,Rename,Speak
  Cat(嵌入 *Animal)的方法集:Intro,Rename,Speak;*Cat 的方法集:Intro,Rename,Speak

关键的一点:Animal.Intro 的接收者是 Animal,它里面的 a.Speak() 永远是 Animal.Speak,不会因为外面包了一层 Dog 就改调 Dog.Speak。要想让被嵌入类型的方法能调用到外层的“重写”,必须把外层的行为作为接口或函数字段传进去。

嵌入接口做装饰器

实测 · 本机执行输出
  经 countWriter 写了 "hello, world!",统计 n = 13,底层缓冲区内容 "hello, world!"
  nil 的嵌入接口:cw2.Write 调用 → panic:runtime error: invalid memory address or nil pointer dereference

嵌入 io.Writer 的结构体自动拥有 Write,再改写一个方法就是一个装饰器,其余方法保持不变。风险是嵌入的接口没有初始化:调用被转发的方法会解引用 nil,panic。

与 encoding/json

实测 · 本机执行输出
  json.Marshal(Outer{Inner{7, ...}, ...}) = {"id":7,"note":"内层备注","name":"名称","outer_note":"外层备注"}
  Outer.Note 解析到的字段:index [2](外层的 Note);Outer.ID 解析到:index [0 0](提升自 Inner)
  reflect 报告 Outer 有 3 个顶层字段(Inner、Name、Note),其中 Inner 的 Anonymous = true

encoding/json 把匿名嵌入的结构体当作字段展开到外层;同名字段按层级与 tag 规则选择。reflect 里能看到嵌入字段的 Anonymous 为 true,以及 FieldByName 返回的是多层的下标路径。这里只展示了最简单的情形,同名字段冲突时的完整规则在 encoding/json 的文档里,我没有逐条验证。

两类编译错误

实测 · 本机执行输出
  main.go:19:8: ambiguous selector c.X
  main.go:20:16: cannot use Q{} (value of struct type Q) as Incer value in variable declaration: Q does not implement Incer (method Inc has pointer receiver)

歧义选择器(同深度同名)与“值没有指针方法”的接口赋值,都是编译期就报错,不会拖到运行时。

分步演示

结构体嵌入
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。方法集用 reflect.Type.Method 列出;JSON 输出是 encoding/json 的真实结果;编译错误是 go build 的原样输出
    • 没有验证:嵌入泛型类型、嵌入接口与方法重叠的细则(Go 1.14 起允许重叠方法集的接口嵌入,本文没有运行)、嵌入对内存布局与对齐的影响、encoding/json 在多层嵌入与 tag 冲突时的全部规则、序列化库(gob、xml)对嵌入的处理
    京ICP备2026067502号-1京公网安备11010502063487号