Go 中的鸭子类型
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 42 课,共 107 课。
“鸭子类型”一词源自这样一句话:“如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。”在 Go 中,这意味着一种类型不需要显式声明自己实现了某个接口,只需要拥有正确的方法。
考虑这个接口和两个完全无关的类型:
type Quacker interface {
Quack() string
}
type Duck struct{}
func (d Duck) Quack() string { return "Quack!" }
type Robot struct{}
func (r Robot) Quack() string { return "Beep-quack!" }
Duck 和 Robot 的定义中都没有提到 Quacker。但它们都满足这个接口,因为它们都有一个签名正确的 Quack() 方法:
func MakeItQuack(q Quacker) {
fmt.Println(q.Quack())
}
func main() {
MakeItQuack(Duck{}) // Quack!
MakeItQuack(Robot{}) // Beep-quack!
}
这种隐式满足机制很强大,因为它允许你在类型已经存在后再定义接口。你可以创建一个与外部包中的类型匹配的接口,而无需修改它们的源代码。这些类型不需要了解你的接口,只需要表现正确即可。
Go 中的鸭子类型让你在保持编译时类型安全的同时,也能获得动态语言的灵活性。编译器会先验证类型是否确实具有所需的方法,然后才允许将其用作接口值。
挑战
简单我们来构建一个消息系统,演示鸭子类型的实际运用。你会创建几种完全无关的类型,它们都能通过一个通用接口发送消息,但没有任何一种类型会显式声明自己实现了这个接口。
你会把代码分到 3 个文件中:
messengers.go:创建 3 个恰好具有相同行为的无关结构体:Phone有一个Number字段。它的SendMessage(text string) string方法会返回SMS to [Number]: [text]Computer有一个Email字段。它的SendMessage(text string) string方法会返回Email to [Email]: [text]Pigeon有一个Name字段。它的SendMessage(text string) string方法会返回[Name] carries: [text]
sender.go:定义一个Messenger接口,该接口要求实现SendMessage(text string) string方法。创建一个名为Broadcast的函数,它接收一个由Messenger组成的切片和一条消息字符串,然后返回一个字符串切片,其中包含对每个消息发送者调用SendMessage的结果。main.go:从输入中读取详细信息,为每种消息发送者类型各创建一个对象,将它们放入一个由Messenger组成的切片中,然后使用Broadcast通过它们发送消息。将每个结果单独打印在一行上。
输入内容如下:
- 第 1 行:电话号码
- 第 2 行:电子邮件地址
- 第 3 行:鸽子名称
- 第 4 行:要广播的消息
例如,输入 555-1234、alice@mail.com、Winston 和 Hello World 后,输出应为:
SMS to 555-1234: Hello World
Email to alice@mail.com: Hello World
Winston carries: Hello World这里的关键在于,Phone、Computer 和 Pigeon 之间没有任何共同之处。它们没有共享基类型,也没有显式实现 Messenger。然而,因为它们的行为都像是在“嘎嘎叫”(拥有匹配的方法),Go 就允许你把它们都用作 Messenger 值。这就是鸭子类型的实际应用。
亲手试试
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 读取电话号码
scanner.Scan()
phoneNumber := scanner.Text()
// 读取电子邮箱地址
scanner.Scan()
email := scanner.Text()
// 读取信鸽的名字
scanner.Scan()
pigeonName := scanner.Text()
// 读取要广播的消息
scanner.Scan()
message := scanner.Text()
// TODO: 为每种信使类型各创建一个实例(Phone、Computer、Pigeon)
// TODO: 将所有信使放入一个 Messenger 类型的切片
// TODO: 使用 Broadcast 通过所有信使发送消息
// TODO: 每行打印一个结果
// 用于使用变量的占位代码(实现时删除)
_ = phoneNumber
_ = email
_ = pigeonName
_ = message
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器