文章节选自《Go语言实战》
多态
现在了解了接口和方法集背后的机制,最后来看一个展示接口的多态行为的例子,如代码所示。
01 // 这个示例程序使用接口展示多态行为
02 package main
03
04 import (
05 "fmt"
06 )
0708 // notifier 是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
13
14 // user 在程序里定义一个用户类型
15 type user struct {
16 name string
17 email string
18 }
19
20 // notify 使用指针接收者实现了 notifier 接口
21 func (u *user) notify() {
22 fmt.Printf("Sending user email to %s<%s>\n",
23 u.name,
24 u.email)
25 }
26
27 // admin 定义了程序里的管理员
28 type admin struct {
29 name string
30 email string
31 }
32
33 // notify 使用指针接收者实现了 notifier 接口
34 func (a *admin) notify() {
35 fmt.Printf("Sending admin email to %s<%s>\n",
36 a.name,
37 a.email)
38 }
39
40 // main 是应用程序的入口
41 func main() {
42 // 创建一个 user 值并传给 sendNotification
43 bill := user{"Bill", "bill@email.com"}
44 sendNotification(&bill)
45
46 // 创建一个 admin 值并传给 sendNotification
47 lisa := admin{"Lisa", "lisa@email.com"}
48 sendNotification(&lisa)
49 }
50
51 // sendNotification 接受一个实现了 notifier 接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54 n.notify()
55 }
我们有了一个展示接口的多态行为的例子。在第 10 行,我们声明了和之前代码清单中一样的 notifier 接口。之后第 15 行到第 25 行,我们声明了一个名为 user的结构,并使用指针接收者实现了 notifier 接口。在第 28 行到第 38 行,我们声明了一个名为 admin 的结构,用同样的形式实现了 notifier 接口。现在,有两个实体类型实现了notifier 接口。
在第 53 行中,我们再次声明了多态函数 sendNotification,这个函数接受一个实现了notifier 接口的值作为参数。既然任意一个实体类型都能实现该接口,那么这个函数可以针对任意实体类型的值来执行 notifier 方法。因此,这个函数就能提供多态的行为,如代码清单 5-49 所示。
代码清单 5-49 listing48.go:第 40 行到第 49 行
40 // main 是应用程序的入口
41 func main() {
42 // 创建一个 user 值并传给 sendNotification
43 bill := user{"Bill", "bill@email.com"}
44 sendNotification(&bill)
45
46 // 创建一个 admin 值并传给 sendNotification
47 lisa := admin{"Lisa", "lisa@email.com"}
48 sendNotification(&lisa)
49 }
最后,可以在代码清单 5-49 中看到这种多态的行为。 main 函数的第 43 行创建了一个 user类型的值,并在第 44 行将该值的地址传给了 sendNotification 函数。这最终会导致执行 user类型声明的 notify 方法。之后,在第 47 行和第 48 行,我们对 admin 类型的值做了同样的事情。最终,因为 sendNotification 接受 notifier 类型的接口值,所以这个函数可以同时执行 user 和 admin 实现的行为。
嵌入类型
Go 语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。这个功能是通过嵌入类型( type embedding)完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。
让我们通过一个示例程序来演示嵌入类型的基本用法,如代码清单 5-50 所示。
代码清单 5-50 listing50.go
01 // 这个示例程序展示如何将一个类型嵌入另一个类型,以及
02 // 内部类型和外部类型之间的关系
03 package main04
05 import (
06 "fmt"
07 )
08
09 // user 在程序里定义一个用户类型
10 type user struct {
11 name string
12 email string
13 }
14
15 // notify 实现了一个可以通过 user 类型值的指针
16 // 调用的方法
17 func (u *user) notify() {
18 fmt.Printf("Sending user email to %s<%s>\n",
19 u.name,
20 u.email)
21 }
22
23 // admin 代表一个拥有权限的管理员用户
24 type admin struct {
25 user // 嵌入类型
26 level string
27 }
28
29 // main 是应用程序的入口
30 func main() {
31 // 创建一个 admin 用户
32 ad := admin{
33 user: user{
34 name: "john smith",
35 email: "john@yahoo.com",
36 },
37 level: "super",
38 }
39
40 // 我们可以直接访问内部类型的方法
41 ad.user.notify()
42
43 // 内部类型的方法也被提升到外部类型
44 ad.notify()
45 }
在代码清单 5-50 中,我们的程序演示了如何嵌入一个类型,并访问嵌入类型的标识符。我们从第 10 行和第 24 行中的两个结构类型的声明开始,如代码清单 5-51 所示。
代码清单 5-51 listing50.go:第 09 行到第 13 行,第 23 行到第 27 行
09 // user 在程序里定义一个用户类型
10 type user struct {
11 name string
12 email string
13 }23 // admin 代表一个拥有权限的管理员用户
24 type admin struct {
25 user // 嵌入类型
26 level string
27 }
在代码清单 5-51 的第 10 行,我们声明了一个名为 user 的结构类型。在第 24 行,我们声明了另一个名为 admin 的结构类型。在声明 admin 类型的第 25 行,我们将 user 类型嵌入 admin类型里。要嵌入一个类型,只需要声明这个类型的名字就可以了。在第 26 行,我们声明了一个名为 level 的字段。注意声明字段和嵌入类型在语法上的不同。一旦我们将 user 类型嵌入 admin,我们就可以说 user 是外部类型 admin 的内部类型。有了内部类型和外部类型这两个概念,就能更容易地理解这两种类型之间的关系。代码清单 5-52 展示了使用 user 类型的指针接收者声明名为 notify 的方法。这个方法只是显示一行友好的信息,表示将邮件发给了特定的用户以及邮件地址。
代码清单 5-52 listing50.go:第 15 行到第 21 行
15 // notify 实现了一个可以通过 user 类型值的指针
16 // 调用的方法
17 func (u *user) notify() {
18 fmt.Printf("Sending user email to %s<%s>\n",
19 u.name,
20 u.email)
21 }
现在,让我们来看一下 main 函数,如代码清单 5-53 所示。
代码清单 5-53 listing50.go:第 30 行到第 45 行
30 func main() {
31 // 创建一个 admin 用户
32 ad := admin{
33 user: user{
34 name: "john smith",
35 email: "john@yahoo.com",
36 },
37 level: "super",
38 }
39
40 // 我们可以直接访问内部类型的方法
41 ad.user.notify()
42
43 // 内部类型的方法也被提升到外部类型
44 ad.notify()
45 }
代码清单 5-53 中的 main 函数展示了嵌入类型背后的机制。在第 32 行,创建了一个 admin
类型的值。内部类型的初始化是用结构字面量完成的。通过内部类型的名字可以访问内部类型,
108 第 5 章 Go 语言的类型系统
如代码清单 5-54 所示。对外部类型来说,内部类型总是存在的。这就意味着,虽然没有指定内
部类型对应的字段名,还是可以使用内部类型的类型名,来访问到内部类型的值。
代码清单 5-54 listing50.go:第 40 行到第 41 行
40 // 我们可以直接访问内部类型的方法
41 ad.user.notify()
在代码清单 5-54 中第 41 行,可以看到对 notify 方法的调用。这个调用是通过直接访问内
部类型 user 来完成的。这展示了内部类型是如何存在于外部类型内,并且总是可访问的。不过,
借助内部类型提升, notify 方法也可以直接通过 ad 变量来访问,如代码清单 5-55 所示。
代码清单 5-55 listing50.go:第 43 行到第 45 行
43 // 内部类型的方法也被提升到外部类型
44 ad.notify()
45 }
代码清单 5-55 的第 44 行中展示了直接通过外部类型的变量来调用 notify 方法。由于内部
类型的标识符提升到了外部类型,我们可以直接通过外部类型的值来访问内部类型的标识符。让
我们修改一下这个例子,加入一个接口,如代码清单 5-56 所示。
代码清单 5-56 listing56.go
01 // 这个示例程序展示如何将嵌入类型应用于接口
02 package main
03
04 import (
05 "fmt"
06 )
07
08 // notifier 是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
13
14 // user 在程序里定义一个用户类型
15 type user struct {
16 name string
17 email string
18 }
19
20 // 通过 user 类型值的指针
21 // 调用的方法
22 func (u *user) notify() {
23 fmt.Printf("Sending user email to %s<%s>\n",
24 u.name,
25 u.email)
26 }27
28 // admin 代表一个拥有权限的管理员用户
29 type admin struct {
30 user
31 level string
32 }
33
34 // main 是应用程序的入口
35 func main() {
36 // 创建一个 admin 用户
37 ad := admin{
38 user: user{
39 name: "john smith",
40 email: "john@yahoo.com",
41 },
42 level: "super",
43 }
44
45 // 给 admin 用户发送一个通知
46 // 用于实现接口的内部类型的方法,被提升到
47 // 外部类型
48 sendNotification(&ad)
49 }
50
51 // sendNotification 接受一个实现了 notifier 接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54 n.notify()
55 }
代码清单 5-56 所示的示例程序的大部分和之前的程序相同,只有一些小变化,如代码清
单 5-57 所示。
代码清单 5-57 第 08 行到第 12 行,第 51 行到第 55 行
08 // notifier 是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11 notify()
12 }
51 // sendNotification 接受一个实现了 notifier 接口的值
52 // 并发送通知
53 func sendNotification(n notifier) {
54 n.notify()
55 }
在代码清单 5-57 的第 08 行,声明了一个 notifier 接口。之后在第 53 行,有一个
sendNotification 函数,接受 notifier 类型的接口的值。从代码可以知道, user 类型之
前声明了名为 notify 的方法,该方法使用指针接收者实现了 notifier 接口。之后,让我们
看一下 main 函数的改动,如代码清单 5-58 所示。
110 第 5 章 Go 语言的类型系统
代码清单 5-58 listing56.go:第 35 行到第 49 行
35 func main() {
36 // 创建一个 admin 用户
37 ad := admin{
38 user: user{
39 name: "john smith",
40 email: "john@yahoo.com",
41 },
42 level: "super",
43 }
44
45 // 给 admin 用户发送一个通知
46 // 用于实现接口的内部类型的方法,被提升到
47 // 外部类型
48 sendNotification(&ad)
49 }
这里才是事情变得有趣的地方。在代码清单 5-58 的第 37 行,我们创建了一个名为 ad 的变
量,其类型是外部类型 admin。这个类型内部嵌入了 user 类型。之后第 48 行,我们将这个外
部类型变量的地址传给 sendNotification 函数。编译器认为这个指针实现了 notifier 接
口,并接受了这个值的传递。不过如果看一下整个示例程序,就会发现 admin 类型并没有实现
这个接口。
由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的
实现,外部类型也同样实现了这个接口。运行这个示例程序,会得到代码清单 5-59 所示的输出。
代码清单 5-59 listing56.go 的输出
20 // 通过 user 类型值的指针
21 // 调用的方法
22 func (u *user) notify() {
23 fmt.Printf("Sending user email to %s<%s>\n",
24 u.name,
25 u.email)
26 }
Output:
Sending user email to john smith<john@yahoo.com>
可以在代码清单 5-59 中看到内部类型的实现被调用。
如果外部类型并不需要使用内部类型的实现,而想使用自己的一套实现,该怎么办?让我们
看另一个示例程序是如何解决这个问题的,如代码清单 5-60 所示。
代码清单 5-60 listing60.go
01 // 这个示例程序展示当内部类型和外部类型要
02 // 实现同一个接口时的做法
03 package main
异步社区会员 database(13669188751) 专享 尊重版权
5.5 嵌入类型 111
04
05 import (
06 "fmt"
07 )
08
08 // notifier 是一个定义了
09 // 通知类行为的接口
11 type notifier interface {
12 notify()
13 }
14
15 // user 在程序里定义一个用户类型
16 type user struct {
17 name string
18 email string
19 }
20
21 // 通过 user 类型值的指针
22 // 调用的方法
23 func (u *user) notify() {
24 fmt.Printf("Sending user email to %s<%s>\n",
25 u.name,
26 u.email)
27 }
28
29 // admin 代表一个拥有权限的管理员用户
30 type admin struct {
31 user
32 level string
33 }
34
35 // 通过 admin 类型值的指针
36 // 调用的方法
37 func (a *admin) notify() {
38 fmt.Printf("Sending admin email to %s<%s>\n",
39 a.name,
40 a.email)
41 }
42
43 // main 是应用程序的入口
44 func main() {
45 // 创建一个 admin 用户
46 ad := admin{
47 user: user{
48 name: "john smith",
49 email: "john@yahoo.com",
50 },
51 level: "super",
52 }
53
54 // 给 admin 用户发送一个通知
55 // 接口的嵌入的内部类型实现并没有提升到
56 // 外部类型
57 sendNotification(&ad)
58
59 // 我们可以直接访问内部类型的方法
60 ad.user.notify()
61
62 // 内部类型的方法没有被提升
63 ad.notify()
64 }
65
66 // sendNotification 接受一个实现了 notifier 接口的值
67 // 并发送通知
68 func sendNotification(n notifier) {
69 n.notify()
70 }
代码清单 5-60 所示的示例程序的大部分和之前的程序相同,只有一些小变化,如代码清
单 5-61 所示。
代码清单 5-61 listing60.go:第 35 行到第 41 行
35 // 通过 admin 类型值的指针
36 // 调用的方法
37 func (a *admin) notify() {
38 fmt.Printf("Sending admin email to %s<%s>\n",
39 a.name,
40 a.email)
41 }
这个示例程序为 admin 类型增加了 notifier 接口的实现。当 admin 类型的实现被调用时,会显示"Sending admin email"。作为对比, user 类型的实现被调用时,会显示"Sending user email"。
main 函数里也有一些变化,如代码清单 5-62 所示。
代码清单 5-62 listing60.go:第 43 行到第 64 行
43 // main 是应用程序的入口
44 func main() {
45 // 创建一个 admin 用户
46 ad := admin{
47 user: user{
48 name: "john smith",
49 email: "john@yahoo.com",
50 },
51 level: "super",
52 }
53
54 // 给 admin 用户发送一个通知
55 // 接口的嵌入的内部类型实现并没有提升到
56 // 外部类型
57 sendNotification(&ad)
58
59 // 我们可以直接访问内部类型的方法
60 ad.user.notify()
61
62 // 内部类型的方法没有被提升
63 ad.notify()
64 }
代码清单 5-62 的第 46 行,我们再次创建了外部类型的变量 ad。在第 57 行,将 ad 变量的
地址传给 sendNotification 函数,这个指针实现了接口所需要的方法集。在第 60 行,代码
直接访问 user 内部类型,并调用 notify 方法。最后,在第 63 行,使用外部类型变量 ad 来
调用 notify 方法。当查看这个示例程序的输出(如代码清单 5-63 所示)时,就会看到区别。
代码清单 5-63 listing60.go 的输出
Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>
这次我们看到了 admin 类型是如何实现 notifier 接口的,以及如何由 sendNotification
函数以及直接使用外部类型的变量 ad 来执行 admin 类型实现的方法。这表明,如果外部类型
实现了 notify 方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以
通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。