类型内嵌

    结构体一文中,我们得知一个结构体类型可以拥有若干字段。 每个字段由一个字段名和一个字段类型组成。事实上,有时,一个字段可以仅由一个字段类型组成。 这样的字段声明方式称为类型内嵌(type embedding)。

    此篇文章将解释类型内嵌的目的和各种和类型内嵌相关的细节。

    下面是一个使用了类型内嵌的例子:

    在上面这个例子中,有六个类型被内嵌在了一个结构体类型中。每个类型内嵌形成了一个内嵌字段(embedded field)。

    因为历史原因,内嵌字段有时也称为匿名字段。但是,事实上,每个内嵌字段有一个(隐式的)名字。 此字段的非限定(unqualified)类型名即为此字段的名称。 比如,上例中的六个内嵌字段的名称分别为、errorintPMHeader

    哪些类型可以被内嵌?

    当前的Go白皮书(1.17)规定

    翻译过来:

    一个内嵌字段必须被声明为形式T或者一个基类型为非接口类型的指针类型*T,其中T为一个类型名但是T不能表示一个指针类型。

    此规则描述在Go 1.9之前是精确的。但是随着从Go 1.9引入的自定义类型别名概念,此描述。 比如,此描述没有包括上一节的例子中的P内嵌字段的情形。

    这里,本文试图使用一个更精确的描述:

    • 一个类型名T只有在它既不表示一个定义的指针类型也不表示一个基类型为指针类型或者接口类型的指针类型的情况下才可以被用作内嵌字段。
    • 一个指针类型*T只有在T为一个类型名并且T既不表示一个指针类型也不表示一个接口类型的时候才能被用作内嵌字段。

    下面列出了一些可以被或不可以被内嵌的类型或别名:

    1. type Encoder interface {Encode([]byte) []byte}
    2. type Person struct {name string; age int}
    3. type Alias = struct {name string; age int}
    4. type AliasPtr = *struct {name string; age int}
    5. type IntPtr *int
    6. type AliasPP = *IntPtr
    7. // 这些类型或别名都可以被内嵌。
    8. Encoder
    9. Person
    10. *Person
    11. Alias
    12. *Alias
    13. AliasPtr
    14. int
    15. *int
    16. // 这些类型或别名都不能被内嵌。
    17. AliasPP // 基类型为一个指针类型
    18. *Encoder // 基类型为一个接口类型
    19. *AliasPtr // 基类型为一个指针类型
    20. IntPtr // 定义的指针类型
    21. *IntPtr // 基类型为一个指针类型
    22. *chan int // 基类型为一个非定义类型
    23. struct {age int} // 非定义非指针类型
    24. map[string]int // 非定义非指针类型
    25. []int64 // 非定义非指针类型
    26. func() // 非定义非指针类型

    一个结构体类型中不允许有两个同名字段,此规则对匿名字段同样适用。 根据上述内嵌字段的隐含名称规则,一个非定义指针类型不能和它的基类型同时内嵌在同一个结构体类型中。 比如,int*int类型不能同时内嵌在同一个结构体类型中。

    一个结构体类型不能内嵌(无论间接还是直接)它自己。

    一般说来,只有内嵌含有字段或者拥有方法的类型才有意义(后续几节将阐述原因),尽管很多既没有字段也没有方法的类型也可以被内嵌。

    类型内嵌的意义是什么?

    类型内嵌的主要目的是为了将被内嵌类型的功能扩展到内嵌它的结构体类型中,从而我们不必再为此结构体类型重复实现被内嵌类型的功能。

    • 如果类型T继承了另外一个类型,则类型T获取了另外一个类型的能力。 同时,一个T类型的值也可以被当作另外一个类型的值来使用。
    • 如果一个类型T内嵌了另外一个类型,则另外一个类型变成了类型T的一部分。 类型T获取了另外一个类型的能力,但是T类型的任何值都不能被当作另外一个类型的值来使用。

    下面是一个展示了如何通过类型内嵌来扩展类型功能的例子:

    1. package main
    2. import "fmt"
    3. type Person struct {
    4. Name string
    5. Age int
    6. }
    7. func (p Person) PrintName() {
    8. fmt.Println("Name:", p.Name)
    9. }
    10. func (p *Person) SetAge(age int) {
    11. p.Age = age
    12. }
    13. type Singer struct {
    14. Person // 通过内嵌Person类型来扩展之
    15. works []string
    16. }
    17. func main() {
    18. var gaga = Singer{Person: Person{"Gaga", 30}}
    19. gaga.PrintName() // Name: Gaga
    20. gaga.Name = "Lady Gaga"
    21. (&gaga).SetAge(31)
    22. (&gaga).PrintName() // Name: Lady Gaga
    23. fmt.Println(gaga.Age) // 31
    24. }

    从上例中,当类型Singer内嵌了类型Person之后,看上去类型Singer获取了类型Person所有的字段和方法, 并且类型*Singer获取了类型*Person所有的方法。此结论是否正确?随后几节将给出答案。

    注意,类型Singer的一个值不能被当作Person类型的值用。下面的代码编译不通过:

    1. var gaga = Singer{}
    2. var _ Person = gaga

    下面这个程序使用反射列出了上一节的例子中的Singer类型的字段和方法,以及*Singer类型的方法。

    1. package main
    2. import (
    3. "fmt"
    4. "reflect"
    5. )
    6. ... // 为节省篇幅,上一个例子中声明的类型在这里省略了。
    7. func main() {
    8. t := reflect.TypeOf(Singer{}) // the Singer type
    9. fmt.Println(t, "has", t.NumField(), "fields:")
    10. fmt.Print(" field#", i, ": ", t.Field(i).Name, "\n")
    11. }
    12. fmt.Println(t, "has", t.NumMethod(), "methods:")
    13. for i := 0; i < t.NumMethod(); i++ {
    14. fmt.Print(" method#", i, ": ", t.Method(i).Name, "\n")
    15. }
    16. pt := reflect.TypeOf(&Singer{}) // the *Singer type
    17. fmt.Println(pt, "has", pt.NumMethod(), "methods:")
    18. for i := 0; i < pt.NumMethod(); i++ {
    19. fmt.Print(" method#", i, ": ", pt.Method(i).Name, "\n")
    20. }
    21. }

    输出结果:

    从此输出结果中,我们可以看出类型Singer确实拥有一个PrintName方法,以及类型*Singer确实拥有两个方法:PrintNameSetAge。 但是类型Singer并不拥有一个Name字段。那么为什么选择器表达式gaga.Name是合法的呢? 毕竟gagaSinger类型的一个值。 请阅读下一节以获取原因。

    选择器的缩写形式

    从前面的结构体和两篇文章中,我们得知,对于一个值xx.y称为一个选择器,其中可以是一个字段名或者方法名。 如果y是一个字段名,那么x必须为一个结构体值或者结构体指针值。 一个选择器是一个表达式,它表示着一个值。 如果选择器x.y表示一个字段,此字段也可能拥有自己的字段(如果此字段的类型为另一个结构体类型)和方法,比如x.y.z,其中z可以是一个字段名,也可是一个方法名。

    在Go中,(不考虑下面将要介绍的选择器碰撞和遮挡),如果一个选择器中的中部某项对应着一个内嵌字段,则此项可被省略掉。 因此内嵌字段又被称为匿名字段。

    一个例子:

    1. package main
    2. type A struct {
    3. x int
    4. }
    5. func (a A) MethodA() {}
    6. type B struct {
    7. *A
    8. }
    9. type C struct {
    10. B
    11. }
    12. func main() {
    13. var c = &C{B: B{A: &A{FieldX: 5}}}
    14. // 这几行是等价的。
    15. _ = c.B.A.FieldX
    16. _ = c.B.FieldX
    17. _ = c.A.FieldX // A是类型C的一个提升字段
    18. _ = c.FieldX // FieldX也是一个提升字段
    19. // 这几行是等价的。
    20. c.B.A.MethodA()
    21. c.B.MethodA()
    22. c.A.MethodA()
    23. c.MethodA() // MethodA是类型C的一个提升方法
    24. }

    这就是为什么在上一节的例子中选择器表达式gaga.Name是合法的, 因为它只不过是gaga.Person.Name的一个缩写形式。

    类似的,选择器gaga.PrintName可以被看作是gaga.Person.PrintName的缩写形式。 但是,我们也可以不把它看作是一个缩写。毕竟,类型Singer确实拥有一个PrintName方法, 尽管此方法是被隐式声明的(请阅读下下节以获得详情)。 同样的原因,选择器(&gaga).PrintName(&gaga).SetAge可以看作(也可以不看作)是(&gaga.Person).PrintName(&gaga.Person).SetAge的缩写。

    Name被称为类型Singer的一个提升字段(promoted field)。 PrintName被称为类型Singer的一个提升方法(promoted method)。

    注意:我们也可以使用选择器gaga.SetAge,但是只有在gaga是一个可寻址的类型为Singer的值的情况下。 它只不过是(&gaga).SetAge的一个语法糖

    在上面的例子中,c.B.A.FieldX称为选择器表达式c.FieldXc.B.FieldXc.A.FieldX的完整形式。 类似的,c.B.A.MethodA可以称为c.MethodAc.B.MethodAc.A.MethodA的完整形式。

    如果一个选择器的完整形式中的所有中部项均对应着一个内嵌字段,则中部项的数量称为此选择器的深度。 比如,上面的例子中的选择器c.MethodA的深度为2,因为此选择器的完整形式为c.B.A.MethodA,并且BA都对应着一个内嵌字段。

    选择器遮挡和碰撞

    一个值x(这里我们总认为它是可寻址的)可能同时拥有多个最后一项相同的选择器,并且这些选择器的中间项均对应着一个内嵌字段。 对于这种情形(假设最后一项为y):

    • 只有深度最浅的一个完整形式的选择器(并且最浅者只有一个)可以被缩写为x.y。 换句话说,x.y表示深度最浅的一个选择器。其它完整形式的选择器被此最浅者所遮挡(压制)。
    • 如果有多个完整形式的选择器同时拥有最浅深度,则任何完整形式的选择器都不能被缩写为x.y。 我们称这些同时拥有最浅深度的完整形式的选择器发生了碰撞。

    举个例子,假设ABC为三个定义类型

    1. type A struct {
    2. x string
    3. }
    4. func (A) y(int) bool {
    5. return false
    6. }
    7. type B struct {
    8. y bool
    9. }
    10. func (B) x(string) {}
    11. type C struct {
    12. B
    13. }

    下面这段代码编译不通过,原因是选择器v1.A.xv1.B.x的深度一样,所以它们发生了碰撞,结果导致它们都不能被缩写为v1.x。 同样的情况发生在选择器v1.A.yv1.B.y身上。

    1. var v1 struct {
    2. A
    3. B
    4. }
    5. func f1() {
    6. _ = v1.x // error: 模棱两可的v1.x
    7. _ = v1.y // error: 模棱两可的v1.y
    8. }

    下面的代码编译没问题。选择器v2.C.B.x被另一个选择器v2.A.x遮挡了,所以v2.x实际上是选择器v2.A.x的缩写形式。 因为同样的原因,v2.y是选择器v2.A.y(而不是选择器v2.C.B.y)的缩写形式。

    1. var v2 struct {
    2. A
    3. C
    4. }
    5. func f2() {
    6. fmt.Printf("%T \n", v2.x) // string
    7. fmt.Printf("%T \n", v2.y) // func(int) bool
    8. }

    一个被遮挡或者碰撞的选择器并不妨碍更深层的选择器被提升,如下例所示中的.M.z

    一个不寻常的但需要注意的细节是:来自不同库包的两个非导出方法(或者字段)将总是被认为是两个不同的标识符,即使它们的名字完全一致。 因此,当它们的属主类型被同时内嵌在同一个结构体类型中的时候,它们绝对不会相互碰撞或者遮挡。 举个例子,下面这个含有两个库包的Go程序编译和运行都没问题。 但是,如果将其中所有出现的m()改为M(),则此程序将编译不过。 原因是A.MB.M碰撞了,导致c.M为一个非法的选择器。

    1. package foo // import "x.y/foo"
    2. import "fmt"
    3. type A struct {
    4. func (a A) m() {
    5. fmt.Println("A", a.n)
    6. }
    7. type I interface {
    8. m()
    9. }
    10. func Bar(i I) {
    11. i.m()
    12. }
    1. package main
    2. import "fmt"
    3. import "x.y/foo"
    4. type B struct {
    5. n bool
    6. }
    7. func (b B) m() {
    8. fmt.Println("B", b.n)
    9. }
    10. type C struct{
    11. foo.A
    12. B
    13. }
    14. func main() {
    15. var c C
    16. c.m() // B false
    17. foo.Bar(c) // A 0
    18. }

    上面已经提到过,类型Singer*Singer都有一个PrintName方法,并且类型*Singer还有一个SetAge方法。 但是,我们从没有为这两个类型声明过这几个方法。这几个方法从哪来的呢?

    事实上,假设结构体类型S内嵌了一个类型(或者类型别名)T,并且此内嵌是合法的,

    • 对内嵌类型T的每一个方法,如果此方法对应的选择器既不和其它选择器碰撞也未被其它选择器遮挡,则编译器将会隐式地为结构体类型S声明一个同样原型的方法。 继而,编译器也将为指针类型*S隐式声明一个相应的方法。
    • 对类型*T的每一个方法,如果此方法对应的选择器既不和其它选择器碰撞也未被其它选择器遮挡,则编译器将会隐式地为类型*S声明一个同样原型的方法。

    简单说来,

    • 类型struct{T}*struct{T}均将获取类型T的所有方法。
    • 类型*struct{T}struct{*T}*struct{*T}都将获取类型*T的所有方法。

    下面展示了编译器为类型Singer*Singer隐式声明的三个(提升)方法:

    1. // 注意:这些声明不是合法的Go语法。这里这样表示只是为了
    2. // 解释目的。它们有助于解释提升方法值是如何被估值的。
    3. func (s Singer) PrintName = s.Person.PrintName
    4. func (s *Singer) PrintName = s.Person.PrintName
    5. func (s *Singer) SetAge = s.Person.SetAge

    右边的部分为各个提升方法相应的完整形式选择器形式。

    从一文中,我们得知我们不能为非定义的结构体类型(和基类型为非定义结构体类型的指针类型)声明方法。 但是,通过类型内嵌,这样的类型也可以拥有方法。

    如果一个结构体类型内嵌了一个实现了一个接口类型的类型(此内嵌类型可以是此接口类型自己),则一般说来,此结构体类型也实现了此接口类型,除非发生了选择器碰撞和遮挡。 比如,上例中的结构体类型和以它为基类型的指针类型均实现了接口类型I

    请注意:一个类型将只会获取它(直接或者间接)内嵌了的类型的方法。 换句话说,一个类型的方法集由为类型直接(显式或者隐式)声明的方法和此类型的底层类型的方法集组成。 比如,在下面的例子中,

    • 类型Age没有方法,因为代码中既没有为它声明任何方法,它也没有内嵌任何类型,。
    • 类型X有两个方法:IsOddDouble。 其中IsOdd方法是通过内嵌类型MyInt而得来的。
    • 类型Y没有方法,因为它所内嵌的类型Age没有方法,另外代码中也没有为它声明任何方法。
    • 类型Z只有一个方法:IsOdd。 此方法是通过内嵌类型MyInt而得来的。 它没有获取到类型XDouble方法,因为它并没有内嵌类型X
    1. type MyInt int
    2. func (mi MyInt) IsOdd() bool {
    3. return mi%2 == 1
    4. }
    5. type Age MyInt
    6. type X struct {
    7. MyInt
    8. }
    9. func (x X) Double() MyInt {
    10. return x.MyInt + x.MyInt
    11. }
    12. type Y struct {
    13. Age
    14. }
    15. type Z X

    提升方法值的正规化和估值

    假设v.m是一个合法的提升方法表达式,在编译时刻,编译器将把此提升方法表达式正规化。 正规化过程分为两步:首先找出此提升方法表达式的完整形式;然后将此完整形式中的隐式取地址和解引用操作均转换为显式操作。

    和其它的规则一样,对于一个已经正规化的方法值表达式v.m,在运行时刻,当v.m被估值的时候,属主实参v的估值结果的一个副本将被存储下来以供后面调用此方法值的时候使用。

    以下面的代码为例:

    • 提升方法表达式s.M1的完整形式为s.T.X.M1。 将此完整形式中的隐式取地址和解引用操作转换为显式操作之后的结果为(*s.T).X.M1。 在运行时刻,属主实参(*s.T).X被估值并且估值结果的一个副本被存储下来以供后用。 此估值结果为1,这就是为什么调用f()总是打印出1
    • 提升方法表达式s.M2的完整形式为s.T.X.M2。 将此完整形式中的隐式取地址和解引用操作转换为显式操作之后的结果为(&(*s.T).X).M2。 在运行时刻,属主实参&(*s.T).X被估值并且估值结果的一个副本被存储下来以供后用。 此估值结果为提升字段s.X(也就是(*s.T).X)的地址。 任何对s.X的修改都可以通过解引用此地址而反映出来,但是对s.T的修改是不会通过此地址反映出来的。 这就是为什么两个g()调用都打印出了2

    接口类型内嵌接口类型

    在本文的最后,让我们来看一个有趣的例子。 此例子程序将陷入死循环并会因堆栈溢出而崩溃退出。 如果你已经理解了多态和类型内嵌,那么就不难理解为什么此程序将死循环。

    1. package main
    2. type I interface {
    3. m()
    4. }
    5. type T struct {
    6. I
    7. }
    8. func main() {
    9. var t T
    10. var i = &t
    11. t.I = i
    12. }