一个例子看懂嵌入
只有类型、没有名字的字段就是嵌入字段。被嵌入类型的字段和方法可以直接在外层类型上访问。
输出:
Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}
嵌入字段的名字就是它的类型名 User。在字面量里引用它(User: User{...}),或者需要内层值本身(a.User)时,都用这个名字。在字面量中不能用简短名字设置被提升的字段:Admin{Name: "Ana"} 会报错 unknown field Name in struct literal of type Admin。
被提升的方法可以实现接口
提升会把被嵌入类型的方法加入外层类型的方法集。所以内层类型实现的每个接口,外层类型也都实现了。
方法集的规则取决于被嵌入的类型。嵌入 T 时,T 的值接收者方法会提升到外层的值和外层的指针上,但 *T 的指针接收者方法只提升到外层的指针上。嵌入 *T 时,两组方法都会提升到两者上。拿不准时,就嵌入指针,或者通过指针使用外层类型。
嵌入指针是有代价的:指针可能是 nil。不设置 Logger 的 Service{} 也能编译通过,但随后调用 svc.Log(...) 就会 panic。
嵌入不是继承
嵌入看起来像子类化,但有三点和 Java 或 Python 不一样。
外层类型不是内层类型。 Admin 不是 User。接收 User 的函数不会接受 Admin;请传入 a.User。
没有虚方法分派。 被提升的方法在嵌入的值上运行,它对外层类型一无所知。如果它调用另一个方法,调用的是它自己类型的版本,即使外层类型定义了同名方法也一样。
输出:
woof
The animal says ...
在基于类的语言里,Speak 会打印“woof”。在 Go 中,d.Speak() 是 d.Animal.Speak() 的简写,这次调用的接收者是 Animal。如果想要随类型变化的行为,就用接口:把一个 Sounder 传给需要它的代码。
内层类型不知道自己被嵌入了。 没有 super。要扩展一个被提升的方法,就在外层类型上定义同名方法,并显式调用内层的方法:
func (a Admin) Greet() string {
return a.User.Greet() + " (admin)"
}
名称冲突与遮蔽
外层类型上的字段或方法会遮蔽同名的被提升字段或方法,就像上面的 Dog.Sound 那样。层级更浅的名字优先。
当同一层级的两个嵌入类型提升了同一个名字时,这个名字就变得有歧义。只要没有代码使用这个有歧义的名字,程序仍然可以编译:
可以用完整路径,或者在外层类型上定义 ID 来解决冲突。
嵌入接口
接口可以嵌入其他接口。标准库就是这样组合出更大的接口的:
type ReadWriter interface {
Reader
Writer
}
结构体也可以嵌入接口。这样结构体就通过你存进这个字段的值实现了该接口,你只需重写关心的方法。这几乎不用写什么代码就实现了装饰器模式:
这里必须显式写 c.Reader.Read(p)。如果在 Read 里写 c.Read(p),就会无限地调用自己。
在测试替身里嵌入接口是一个常见的捷径:嵌入那个很大的接口,只实现测试需要的那一个方法,其余的不管。任何没有实现的方法一旦被调用就会因空指针解引用而 panic,在测试里这往往正是你想要的。
一个常见的实际用法:嵌入 sync.Mutex
type Stats struct {
sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(page string) {
s.Lock()
defer s.Unlock()
s.hits[page]++
}
这样写读起来很顺,但它也把 Lock 和 Unlock 导出成了 Stats API 的一部分,任何调用方都能给你的结构体加锁。对于在包外使用的类型,用具名字段(mu sync.Mutex)可以让锁保持私有。这个取舍适用于所有嵌入:被嵌入类型导出的一切,都会成为你的类型公开接口的一部分。
常见错误
- 以为有虚方法分派。 被提升的方法永远不会调用外层类型重写的方法。
- 在字面量中设置被提升的字段。 请使用被嵌入类型的名字:
Admin{User: User{Name: "Ana"}}。 - 嵌入的指针为 nil。 嵌入的
*T或接口必须先赋值,才能调用它的方法。 - 意外暴露的 API。 嵌入会把内层类型所有导出的方法都暴露在你的类型上。
常见问题
Go 中的结构体嵌入是什么?
只写类型、不写名字地声明一个字段:type Admin struct { User; Level int }。被嵌入的 User 的字段和方法会被提升,所以可以直接在 Admin 上使用 a.Name 和 a.Greet()。被嵌入的值仍然是一个普通字段,可以通过 a.User 访问。
Go 有继承吗?
没有。Go 没有类,也没有子类型继承。嵌入通过组合实现代码复用:外层类型获得内层类型的方法,但 Admin 并不是 User。在需要 User 的地方不能传入 Admin,被提升的方法也无法回调外层类型的方法。Go 的多态来自接口。
Go 中如何初始化嵌入的结构体?
在复合字面量中,用类型名作为嵌入字段的名字:Admin{User: User{Name: "Ana"}, Level: 2}。不能在字面量中直接设置被提升的字段:Admin{Name: "Ana"} 会报错 unknown field Name in struct literal of type Admin。
Go 中可以在结构体里嵌入接口吗?
可以。这样结构体就通过嵌入的值实现了该接口,你还可以只重写其中个别方法。这在装饰器和测试替身中很常见。如果嵌入的接口字段是 nil,调用没有重写的方法会因为空指针解引用而 panic。