返回介绍

5.4 接口

发布于 2024-10-11 12:39:00 字数 12450 浏览 0 评论 0 收藏 0

多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。标准库里有很好的例子,如 io 包里实现的流式处理接口。 io 包提供了一组构造得非常好的接口和函数,来让代码轻松支持流式数据处理。只要实现两个接口,就能利用整个 io 包背后的所有强大能力。

不过,我们的程序在声明和实现接口时会涉及很多细节。即便实现的是已有接口,也需要了解这些接口是如何工作的。在探究接口如何工作以及实现的细节之前,我们先来看一下使用标准库里的接口的例子。

5.4.1 标准库

我们先来看一个示例程序,这个程序实现了流行程序 curl 的功能,如代码清单 5-34 所示。

代码清单 5-34 listing34.go

01 // 这个示例程序展示如何使用 io.Reader 和 io.Writer 接口
02 // 写一个简单版本的 curl 程序
03 package main
04
05 import (
06   "fmt"
07   "io"
08   "net/http"
09   "os"
10 )
11
12 // init 在 main 函数之前调用
13 func init() {
14   if len(os.Args) != 2 {
15     fmt.Println("Usage: ./example2 <url>")
16     os.Exit(-1)
17   }
18 }
19
20 // main 是应用程序的入口
21 func main() {
22   // 从 Web 服务器得到响应
23   r, err := http.Get(os.Args[1])
24   if err != nil {
25     fmt.Println(err)
26     return
27   }
28
29   // 从 Body 复制到 Stdout
30   io.Copy(os.Stdout, r.Body)
31   if err := r.Body.Close(); err != nil {
32     fmt.Println(err)
33   }
34 }

代码清单 5-34 展示了接口的能力以及在标准库里的应用。只用了几行代码我们就通过两个函数以及配套的接口,完成了 curl 程序。在第 23 行,调用了 http 包的 Get 函数。在与服务器成功通信后, http.Get 函数会返回一个 http.Response 类型的指针。 http.Response 类型包含一个名为 Body 的字段,这个字段是一个 io.ReadCloser 接口类型的值。

在第 30 行, Body 字段作为第二个参数传给 io.Copy 函数。 io.Copy 函数的第二个参数,接受一个 io.Reader 接口类型的值,这个值表示数据流入的源。 Body 字段实现了 io.Reader 接口,因此我们可以将 Body 字段传入 io.Copy ,使用 Web 服务器的返回内容作为源。

io.Copy 的第一个参数是复制到的目标,这个参数必须是一个实现了 io.Writer 接口的值。对于这个目标,我们传入了 os 包里的一个特殊值 Stdout 。这个接口值表示标准输出设备,并且已经实现了 io.Writer 接口。当我们将 BodyStdout 这两个值传给 io.Copy 函数后,这个函数会把服务器的数据分成小段,源源不断地传给终端窗口,直到最后一个片段读取并写入终端, io.Copy 函数才返回。

io.Copy 函数可以以这种工作流的方式处理很多标准库里已有的类型,如代码清单 5-35 所示。

代码清单 5-35 listing35.go

01 // 这个示例程序展示 bytes.Buffer 也可以
02 // 用于 io.Copy 函数
03 package main
04
05 import (
06   "bytes"
07   "fmt"
08   "io"
09   "os"
10 )
11
12 // main 是应用程序的入口
13 func main() {
14   var b bytes.Buffer
15
16   // 将字符串写入 Buffer
17   b.Write([]byte("Hello"))
18
19   // 使用 Fprintf 将字符串拼接到 Buffer
20   fmt.Fprintf(&b, "World!")
21
22   // 将 Buffer 的内容写到 Stdout
23   io.Copy(os.Stdout, &b)
24 }

代码清单 5-35 展示了一个程序,这个程序使用接口来拼接字符串,并将数据以流的方式输出到标准输出设备。在第 14 行,创建了一个 bytes 包里的 Buffer 类型的变量 b ,用于缓冲数据。之后在第 17 行使用 Write 方法将字符串 Hello 写入这个缓冲区 b 。第 20 行,调用 fmt 包里的 Fprintf 函数,将第二个字符串追加到缓冲区 b 里。

fmt.Fprintf 函数接受一个 io.Writer 类型的接口值作为其第一个参数。由于 bytes.Buffer 类型的指针实现了 io.Writer 接口,所以可以将缓存 b 传入 fmt.Fprintf 函数,并执行追加操作。最后,在第 23 行,再次使用 io.Copy 函数,将字符写到终端窗口。由于 bytes.Buffer 类型的指针也实现了 io.Reader 接口, io.Copy 函数可以用于在终端窗口显示缓冲区 b 的内容。

希望这两个小程序展示出接口的好处,以及标准库内部是如何使用接口的。下一步,让我们看一下实现接口的细节。

5.4.2 实现

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。

对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态。在这个关系里,用户定义的类型通常叫作 实体类型 ,原因是如果离开内部存储的用户定义的类型的值的实现,接口值并没有具体的行为。

并不是所有值都完全等同,用户定义的类型的值或者指针要满足接口的实现,需要遵守一些规则。这些规则在 5.4.3 节介绍方法集时有详细说明。探寻方法集的细节之前,了解接口类型值大概的形式以及用户定义的类型的值是如何存入接口的,会有很多帮助。

图 5-1 展示了在 user 类型值赋值后接口变量的值的内部布局。接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作 iTable,包含了所存储的值的类型信息。iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系。

..\17-0021 改图\0501.tif

图 5-1 实体值赋值后接口值的简图

图 5-2 展示了一个指针赋值给接口之后发生的变化。在这种情况里,类型信息会存储一个指向保存的类型的指针,而接口值第二个字依旧保存指向实体值的指针。

..\0502.tif

图 5-2 实体指针赋值后接口值的简图

5.4.3 方法集

方法集定义了接口的接受规则。看一下代码清单 5-36 所示的代码,有助于理解方法集在接口中的重要角色。

代码清单 5-36 listing36.go

01 // 这个示例程序展示 Go 语言里如何使用接口
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 // notify 是使用指针接收者实现的方法
21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }
26
27 // main 是应用程序的入口
28 func main() {
29   // 创建一个 user 类型的值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将 u(类型是 user)作为
35   //            sendNotification 的参数类型 notifier:
36   //  user 类型并没有实现 notifier
37   //                (notify 方法使用指针接收者声明)
38 }
39
40 // sendNotification 接受一个实现了 notifier 接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43   n.notify()
44 }

代码清单 5-36 中的程序虽然看起来没问题,但实际上却无法通过编译。在第 10 行中,声明了一个名为 notifier 的接口,包含一个名为 notify 的方法。第 15 行中,声明了名为 user 的实体类型,并通过第 21 行中的方法声明实现了 notifier 接口。这个方法是使用 user 类型的指针接收者实现的。

代码清单 5-37 listing36.go:第 40 行到第 44 行

40 // sendNotification 接受一个实现了 notifier 接口的值
41 // 并发送通知
42 func sendNotification(n notifier) {
43   n.notify()
44 }

在代码清单 5-37 的第 42 行,声明了一个名为 sendNotification 的函数。这个函数接收一个 notifier 接口类型的值。之后,使用这个接口值来调用 notify 方法。任何一个实现了 notifier 接口的值都可以传入 sendNotification 函数。现在让我们来看一下 main 函数,如代码清单 5-38 所示。

代码清单 5-38 listing36.go:第 28 行到第 38 行

28 func main() {
29   // 创建一个 user 类型的值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将 u(类型是 user)作为
35   //            sendNotification 的参数类型 notifier:
36   //  user 类型并没有实现 notifier
37   //                (notify 方法使用指针接收者声明)
38 }

main 函数里,代码清单 5-38 的第 30 行,创建了一个 user 实体类型的值,并将其赋值给变量 u。之后在第 32 行将 u 的值传入 sendNotification 函数。不过,调用 sendNotification 的结果是产生了一个编译错误,如代码清单 5-39 所示。

代码清单 5-39 将 user 类型的值存入接口值时产生的编译错误

./listing36.go:32: 不能将 u(类型是 user)作为 sendNotification 的参数类型 notifier:
 user 类型并没有实现 notifier(notify 方法使用指针接收者声明)

既然 user 类型已经在第 21 行实现了 notify 方法,为什么这里还是产生了编译错误呢?让我们再来看一下那段代码,如代码清单 5-40 所示。

代码清单 5-40 listing36.go:第 08 行到第 12 行,第 21 行到第 25 行

08 // notifier 是一个定义了
09 // 通知类行为的接口
10 type notifier interface {
11   notify()
12 }

21 func (u *user) notify() {
22   fmt.Printf("Sending user email to %s<%s>\n",
23     u.name,
24     u.email)
25 }

代码清单 5-40 展示了接口是如何实现的,而编译器告诉我们 user 类型的值并没有实现这个接口。如果仔细看一下编译器输出的消息,其实编译器已经说明了原因,如代码清单 5-41 所示。

代码清单 5-41 进一步查看编译器错误

(notify method has pointer receiver)

要了解用指针接收者来实现接口时为什么 user 类型的值无法实现该接口,需要先了解 方法集 。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。

让我们先解释一下 Go 语言规范里定义的方法集的规则,如代码清单 5-42 所示。

代码清单 5-42 规范里描述的方法集

Values        Methods Receivers
-----------------------------------------------
  T          (t T)
  *T          (t T) and (t *T)

代码清单 5-42 展示了规范里对方法集的描述。描述中说到, T 类型的值的方法集只包含值接收者声明的方法。而指向 T 类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。从值的角度看这些规则,会显得很复杂。让我们从接收者的角度来看一下这些规则,如代码清单 5-43 所示。

代码清单 5-43 从接收者类型的角度来看方法集

Methods Receivers   Values 
-----------------------------------------------
  (t T)         T and *T
  (t *T)         *T

代码清单 5-43 展示了同样的规则,只不过换成了接收者的视角。这个规则说,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。现在再看一下代码清单 5-36 所示的代码,就能理解出现编译错误的原因了,如代码清单 5-44 所示。

代码清单 5-44 listing36.go:第 28 行到第 38 行

28 func main() {
29   // 使用 user 类型创建一个值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(u)
33
34   // ./listing36.go:32: 不能将 u(类型是 user)作为
35   //            sendNotification 的参数类型 notifier:
36   //  user 类型并没有实现 notifier
37   //                (notify 方法使用指针接收者声明)
38 }

我们使用指针接收者实现了接口,但是试图将 user 类型的值传给 sendNotification 方法。代码清单 5-44 的第 30 行和第 32 行清晰地展示了这个问题。但是,如果传递的是 user 值的地址,整个程序就能通过编译,并且能够工作了,如代码清单 5-45 所示。

代码清单 5-45 listing36.go:第 28 行到第 35 行

28 func main() {
29   // 使用 user 类型创建一个值,并发送通知
30   u := user{"Bill", "bill@email.com"}
31
32   sendNotification(&u)
33
34   // 传入地址,不再有错误
35 }

在代码清单 5-45 里,这个程序终于可以编译并且运行。因为使用指针接收者实现的接口,只有 user 类型的指针可以传给 sendNotification 函数。

现在的问题是,为什么会有这种限制?事实上,编译器并不是总能自动获得一个值的地址,如代码清单 5-46 所示。

代码清单 5-46 listing46.go

01 // 这个示例程序展示不是总能
02 // 获取值的地址
03 package main
04
05 import "fmt"
06
07 // duration 是一个基于 int 类型的类型
08 type duration int
09
10 // 使用更可读的方式格式化 duration 值
11 func (d *duration) pretty() string {
12   return fmt.Sprintf("Duration: %d", *d)
13 }
14
15 // main 是应用程序的入口
16 func main() {
17   duration(42).pretty()
18
19   // ./listing46.go:17: 不能通过指针调用 duration(42) 的方法
20   // ./listing46.go:17: 不能获取 duration(42) 的地址
21 }

代码清单 5-46 所示的代码试图获取 duration 类型的值的地址,但是获取不到。这展示了不能总是获得值的地址的一种情况。让我们再看一下方法集的规则,如代码清单 5-47 所示。

代码清单 5-47 再看一下方法集的规则

Values        Methods Receivers
-----------------------------------------------
  T          (t T)
  *T          (t T) and (t *T)

 Methods Receivers   Values
-----------------------------------------------
  (t T)         T and *T
  (t *T)        *T

因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法。

5.4.4 多态

现在了解了接口和方法集背后的机制,最后来看一个展示接口的多态行为的例子,如代码清单 5-48 所示。

代码清单 5-48 listing48.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 // 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 }

在代码清单 5-48 中,我们有了一个展示接口的多态行为的例子。在第 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 类型的接口值,所以这个函数可以同时执行 useradmin 实现的行为。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文