类型内嵌
从结构体一文中,我们得知一个结构体类型可以拥有若干字段。每个字段由一个字段名和一个字段类型组成。事实上,有时,一个字段可以仅由一个字段类型组成。这样的字段声明方式称为类型内嵌(type embedding)。
此篇文章将解释类型内嵌的目的和各种和类型内嵌相关的细节。
类型内嵌语法
下面是一个使用了类型内嵌的例子:
package main
import "net/http"
func main() {
type P = *bool
type M = map[int]int
var x struct {
string // 一个定义的非指针类型
error // 一个定义的接口类型
*int // 一个非定义指针类型
P // 一个非定义指针类型的别名
M // 一个非定义类型的别名
http.Header // 一个定义的映射类型
}
x.string = "Go"
x.error = nil
x.int = new(int)
x.P = new(bool)
x.M = make(M)
x.Header = http.Header{}
}
在上面这个例子中,有六个类型被内嵌在了一个结构体类型中。每个类型内嵌形成了一个内嵌字段(embedded field)。
因为历史原因,内嵌字段有时也称为匿名字段。但是,事实上,每个内嵌字段有一个(隐式的)名字。此字段的非限定(unqualified)类型名即为此字段的名称。比如,上例中的六个内嵌字段的名称分别为string
、error
、int
、P
、M
和Header
。
哪些类型可以被内嵌?
当前的Go白皮书(1.13)规定:
An embedded field must be specified as a type nameT
or as a pointer to a non-interface type name*T
, andT
itself may not be a pointer type.
翻译过来:
一个内嵌字段必须被声明为形式T
或者一个基类型为非接口类型的指针类型*T
,其中T
为一个类型名但是T
不能表示一个指针类型。
此规则描述在Go 1.9之前是精确的。但是随着从Go 1.9引入的自定义类型别名概念,此描述有些过时和不太准确了。比如,此描述没有包括上一节的例子中的P
内嵌字段的情形。
这里,本文试图使用一个更精确的描述:
- 一个类型名
T
只有在它既不表示一个定义的指针类型也不表示一个基类型为指针类型或者接口类型的指针类型的情况下在可以被用作内嵌字段。 - 一个指针类型
T
只有在T
为一个类型名并且T
既不表示一个指针类型也不表示一个接口类型的时候才能被用作内嵌字段。<!—这里,本文试图使用一个更精确的描述:- 一个类型名
T
只有在它不表示一个基类型为指针类型或者接口类型的指针类型的情况下在可以被用作内嵌字段。 - 一个指针类型
T
只有在T
为一个类型名并且T
既不表示一个指针类型也不表示一个接口类型的时候才能被用作内嵌字段。
一个内嵌字段必须被声明为形式—>下面列出了一些可以被或不可以被内嵌的类型或别名:T
或者T
,其中T
为一个类型名并且T
和T
不能表示基类型为指针类型或者接口类型的指针类型。 - 一个类型名
type Encoder interface {Encode([]byte) []byte}
type Person struct {name string; age int}
type Alias = struct {name string; age int}
type AliasPtr = *struct {name string; age int}
type IntPtr *int
type AliasPP = *IntPtr
// 这些类型或别名都可以被内嵌。
Encoder
Person
*Person
Alias
*Alias
AliasPtr
int
*int
// 这些类型或别名都不能被内嵌。
AliasPP // 基类型为一个指针类型
*Encoder // 基类型为一个接口类型
*AliasPtr // 基类型为一个指针类型
IntPtr // 定义的指针类型
*IntPtr // 基类型为一个指针类型
*chan int // 基类型为一个非定义类型
struct {age int} // 非定义非指针类型
map[string]int // 非定义非指针类型
[]int64 // 非定义非指针类型
func() // 非定义非指针类型
一个结构体类型中不允许有两个同名字段,此规则对匿名字段同样适用。根据上述内嵌字段的隐含名称规则,一个非定义指针类型不能和它的基类型同时内嵌在同一个结构体类型中。比如,int
和*int
类型不能同时内嵌在同一个结构体类型中。
一个结构体类型不能内嵌(无论间接还是直接)它自己。
一般说来,只有内嵌含有字段或者拥有方法的类型才有意义(后续几节将阐述原因),尽管很多既没有字段也没有方法的类型也可以被内嵌。
类型内嵌的意义是什么?
类型内嵌的主要目的是为了将被内嵌类型的功能扩展到内嵌它的结构体类型中,从而我们不必再为此结构体类型重复实现一下被内嵌类型的功能。 很多其它流行面向对象的编程语言都是用继承来实现上述目的。两种实现方式有它们各自的利弊。这里,此篇文章将不讨论哪种方式更好一些,我们只需知道Go选择了类型内嵌这种方式。这两种方式有一个很大的不同点:
- 如果类型
T
继承了另外一个类型,则类型T
获取了另外一个类型的能力。同时,一个T
类型的值也可以被当作另外一个类型的值来使用。 - 如果一个类型
T
内嵌了另外一个类型,则另外一个类型变成了类型T
的一部分。类型T
获取了另外一个类型的能力,但是T
类型的任何值都不能被当作另外一个类型的值来使用。下面是一个展示了如何通过类型内嵌来扩展类型功能的例子:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) PrintName() {
fmt.Println("Name:", p.Name)
}
func (p *Person) SetAge(age int) {
p.Age = age
}
type Singer struct {
Person // 通过内嵌Person类型来扩展之
works []string
}
func main() {
var gaga = Singer{Person: Person{"Gaga", 30}}
gaga.PrintName() // Name: Gaga
gaga.Name = "Lady Gaga"
(&gaga).SetAge(31)
(&gaga).PrintName() // Name: Lady Gaga
fmt.Println(gaga.Age) // 31
}
从上例中,当类型Singer
内嵌了类型Person
之后,看上去类型Singer
获取了类型Person
所有的字段和方法,并且类型Singer
获取了类型Person
所有的方法。此结论是否正确?随后几节将给出答案。
注意,类型Singer
的一个值不能被当作Person
类型的值用。下面的代码编译不通过:
var gaga = Singer{}
var _ Person = gaga
当一个结构体类型内嵌了另一个类型,此结构体类型是否获取了被内嵌类型的字段和方法?
下面这个程序使用反射列出了上一节的例子中的Singer
类型的字段和方法,以及*Singer
类型的方法。
package main
import (
"fmt"
"reflect"
)
... // 为节省篇幅,上一个例子中声明的类型在这里省略了。
func main() {
t := reflect.TypeOf(Singer{}) // the Singer type
fmt.Println(t, "has", t.NumField(), "fields:")
for i := 0; i < t.NumField(); i++ {
fmt.Print(" field#", i, ": ", t.Field(i).Name, "\n")
}
fmt.Println(t, "has", t.NumMethod(), "methods:")
for i := 0; i < t.NumMethod(); i++ {
fmt.Print(" method#", i, ": ", t.Method(i).Name, "\n")
}
pt := reflect.TypeOf(&Singer{}) // the *Singer type
fmt.Println(pt, "has", pt.NumMethod(), "methods:")
for i := 0; i < pt.NumMethod(); i++ {
fmt.Print(" method#", i, ": ", pt.Method(i).Name, "\n")
}
}
输出结果:
main.Singer has 2 fields:
field#0: Person
field#1: works
main.Singer has 1 methods:
method#0: PrintName
*main.Singer has 2 methods:
method#0: PrintName
method#1: SetAge
从此输出结果中,我们可以看出类型Singer
确实拥有一个PrintName
方法,以及类型*Singer
确实拥有两个方法:PrintName
和SetAge
。但是类型Singer
并不拥有一个Name
字段。那么为什么选择器表达式gaga.Name
是合法的呢?毕竟gaga
是Singer
类型的一个值。请阅读下一节以获取原因。
选择器的缩写形式
从前面的结构体和方法两篇文章中,我们得知,对于一个值x
,x.y
称为一个选择器,其中y
可以是一个字段名或者方法名。如果y
是一个字段名,那么x
必须为一个结构体值或者结构体指针值。一个选择器是一个表达式,它表示着一个值。如果选择器x.y
表示一个字段,此字段也可能拥有自己的字段(如果此字段的类型为另一个结构体类型)和方法,比如x.y.z
,其中z
可以是一个字段名,也可是一个方法名。
在Go中,(不考虑下面将要介绍的选择器碰撞和遮挡),如果一个选择器中的中部某项对应着一个内嵌字段,则此项可被省略掉。这是为什么内嵌字段又被称为匿名字段的原因。
一个例子:
package main
type A struct {
x int
}
func (a A) MethodA() {}
type B struct {
A
}
type C struct {
B
}
func main() {
var c C
// 下面的赋值语句中的选择器都是相互等价的。
_ = c.B.A.x
_ = c.B.x
_ = c.A.x
_ = c.x // x被称为类型C的一个提升字段
// 这下面的几个方法调用都是互相等价的。
c.B.A.MethodA()
c.B.MethodA()
c.A.MethodA()
c.MethodA()
}
这就是为什么在上一节的例子中选择器表达式gaga.Name
是合法的原因,因为它只不过是gaga.Person.Name
的一个缩写形式。Name
被称为类型Singer
的提升字段(promoted field)。
因为任何内嵌其它类型的类型必然是结构体类型,并且结构体一文中已经了我们可以通过一个可寻址的结构体值的指针选择此结构体的字段,所以下面的代码也是合法的:
func main() {
var c C
pc = &c
// 这几行等价。
fmt.Println(pc.B.A.x)
fmt.Println(pc.B.x)
fmt.Println(pc.A.x)
fmt.Println(pc.x)
// 这几行等价。
pc.B.A.MethodA()
pc.B.MethodA()
pc.A.MethodA()
pc.MethodA()
}
类似的,选择器gaga.PrintName
可以被看作是gaga.Person.PrintName
的缩写形式。但是,我们也可以不把它看作是一个缩写。毕竟,类型Singer
确实拥有一个PrintName
方法,尽管此方法是被隐式声明的(请阅读下下节以获得详情)。同样的原因,选择器(&gaga).PrintName
和(&gaga).SetAge
可以看作(也可以不看作)是(&gaga.Person).PrintName
和(&gaga.Person).SetAge
的缩写。
注意:我们也可以使用选择器gaga.SetAge
,但是只有在gaga
是一个可寻址的类型为Singer
的值的情况下。它只不过是(&gaga).SetAge
的一个语法糖。
在上面的例子中,c.B.A.x
称为选择器表达式c.x
、c.B.x
和c.A.x
的完整形式。类似的,c.B.A.MethodA
可以称为c.MethodA
、c.B.MethodA
和c.A.MethodA
的完整形式。
如果一个选择器的完整形式中的所有中部项均对应着一个内嵌字段,则中部项的数量称为此选择器的深度。比如,上面的例子中的选择器c.MethodA
的深度为2,因为此选择器的完整形式为c.B.A.MethodA
,并且B
和A
都对应着一个内嵌字段。
选择器遮挡和碰撞
一个值x
(这里我们总认为它是可寻址的)可能同时拥有多个最后一项相同的选择器,并且这些选择器的中间项均对应着一个内嵌字段。对于这种情形(假设最后一项为y
):
- 只有深度最浅的一个完整形式的选择器(并且最浅者只有一个)可以被缩写为
x.y
。换句话说,x.y
表示深度最浅的一个选择器。其它完整形式的选择器被此最浅者所遮挡(压制)。 - 如果有多个完整形式的选择器同时拥有最浅深度,则任何完整形式的选择器都不能被缩写为
x.y
。我们称这些同时拥有最浅深度的完整形式的选择器发生了碰撞。 如果一个方法选择器被另一个方法选择器所遮挡,并且它们对应的方法原型是一致的,那么我们可以说第一个方法被第二个覆盖(overridden)了。 举个例子,假设A
、B
和C
为三个定义类型:
type A struct {
x string
}
func (A) y(int) bool {
return false
}
type B struct {
y bool
}
func (B) x(string) {}
type C struct {
B
}
下面这段代码编译不通过,原因是选择器v1.A.x
和v1.B.x
发生了碰撞,结果导致它们都不能被缩写为v1.x
。同样的情况发生在选择器v1.A.y
和v1.B.y
身上。
var v1 struct {
A
B
}
func f1() {
_ = v1.x
_ = v1.y
}
下面的代码编译没问题。选择器v2.C.B.x
被另一个选择器v2.A.x
遮挡了,所以v2.x
实际上是选择器v2.A.x
的缩写形式。因为同样的原因,v2.y
是选择器v2.A.y
(而不是选择器v2.C.B.y
)的缩写形式。
var v2 struct {
A
C
}
func f2() {
fmt.Printf("%T \n", v2.x) // string
fmt.Printf("%T \n", v2.y) // func(int) bool
}
为内嵌了其它类型的结构体类型声明的隐式方法
上面已经提到过,类型Singer
和Singer
都有一个PrintName
方法,并且类型Singer
还有一个SetAge
方法。但是,我们从没有为这两个类型声明过这几个方法。这几个方法从哪来的呢?
事实上,假设结构体类型S
内嵌了一个类型T
,并且此内嵌是合法的,
- 对内嵌类型
T
的每一个方法,如果此方法对应的选择器既不和其它选择器碰撞也未被其它选择器遮挡,则编译器将会隐式地为结构体类型S
声明一个同样原型的方法。继而,编译器也将为指针类型*S
隐式声明一个相应的方法。 - 对类型
T
的每一个方法,如果此方法对应的选择器既不和其它选择器碰撞也未被其它选择器遮挡,则编译器将会隐式地为类型S
声明一个同样原型的方法。 上述事实在类型T
不可内嵌的情况下(即T
是一个指针或者接口类型时)仍存在。如果即T
是一个指针或者接口类型时,则类型T
的方法集为空。 简单说来, - 类型
struct{T}
和*struct{T}
将获取类型T
的所有方法。 - 类型
struct{T}
、struct{
T}
和struct{T}
将获取类型T
的所有方法。下面展示了编译器为类型Singer
和Singer
隐式声明的三个方法:
func (s Singer) PrintName() {
s.Person.PrintName()
}
func (s *Singer) PrintName() {
(*s).Person.PrintName()
}
func (s *Singer) SetAge(age int) {
(&s.Person).SetAge(age) // <=> (&((*s).Person)).SetAge(age)
}
从方法一文中,我们得知我们不能为非定义的结构体类型(和基类型为非定义结构体类型的指针类型)声明方法。但是,通过类型内嵌,这样的类型也可以拥有方法。 下面是另一个证明了类型内嵌导致了一些方法被隐式声明的例子:
package main
import "fmt"
import "reflect"
type F func(int) bool
func (f F) Validate(n int) bool {
return f(n)
}
func (f *F) Modify(f2 F) {
*f = f2
}
type B bool
func (b B) IsTrue() bool {
return bool(b)
}
func (pb *B) Invert() {
*pb = !*pb
}
type I interface {
Load()
Save()
}
func PrintTypeMethods(t reflect.Type) {
fmt.Println(t, "has", t.NumMethod(), "methods:")
for i := 0; i < t.NumMethod(); i++ {
fmt.Print(" method#", i, ": ", t.Method(i).Name, "\n")
}
}
func main() {
var s struct {
F
*B
I
}
PrintTypeMethods(reflect.TypeOf(s))
fmt.Println()
PrintTypeMethods(reflect.TypeOf(&s))
}
输出结果:
struct { main.F; *main.B; main.I } has 5 methods:
method#0: Invert
method#1: IsTrue
method#2: Load
method#3: Save
method#4: Validate
*struct { main.F; *main.B; main.I } has 6 methods:
method#0: Invert
method#1: IsTrue
method#2: Load
method#3: Modify
method#4: Save
method#5: Validate
如果一个结构体类型内嵌了一个实现了一个接口类型的类型(此内嵌类型可以是此接口类型自己),则一般说来,此结构体类型也实现了此接口类型,除非发生了选择器碰撞和遮挡。比如,上例中的结构体类型和以它为基类型的指针类型均实现了接口类型I
。
请注意:一个类型将只会获取它(直接或者间接)内嵌了的类型的方法。比如,在下面的例子中,
- 类型
Age
没有方法,因为它没有内嵌任何类型。 - 类型
X
有两个方法:IsOdd
和Double
。其中IsOdd
方法是通过内嵌类型MyInt
而得来的。 - 类型
Y
没有方法,因为它所内嵌的类型Age
没有方法。 - 类型
Z
只有一个方法:IsOdd
。此方法是通过内嵌类型MyInt
而得来的。它没有获取到类型X
的Double
方法,因为它并没有内嵌类型X
。
type MyInt int
func (mi MyInt) IsOdd() bool {
return mi%2 == 1
}
type Age MyInt
type X struct {
MyInt
}
func (x X) Double() MyInt {
return x.MyInt + x.MyInt
}
type Y struct {
Age
}
type Z X
接口类型内嵌接口类型
不但结构体类型可以内嵌类型,接口类型也可以内嵌类型。但是接口类型只能内嵌接口类型。详情请阅读接口一文。
一个有趣的类型内嵌的例子
在本文的最后,让我们来看一个有趣的例子。此例子程序将陷入死循环并会因堆栈溢出而崩溃退出。如果你已经理解了多态和类型内嵌,那么就不难理解为什么此程序将死循环。
package main
type I interface {
m()
}
type T struct {
I
}
func main() {
var t T
var i = &t
t.I = i
i.m() // 将调用t.m(),然后再次调用i.m(),...
}
Go语言101项目目前同时托管在Github和Gitlab上。欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。
本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。