Menu
CoddyTech

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 中的鸭子类型让你在保持编译时类型安全的同时,也能获得动态语言的灵活性。编译器会先验证类型是否确实具有所需的方法,然后才允许将其用作接口值。

challenge icon

挑战

简单

我们来构建一个消息系统,演示鸭子类型的实际运用。你会创建几种完全无关的类型,它们都能通过一个通用接口发送消息,但没有任何一种类型会显式声明自己实现了这个接口。

你会把代码分到 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
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器