diff --git a/chap-16-interfaces/16-interfaces.md b/chap-16-interfaces/16-interfaces.md new file mode 100644 index 0000000000000000000000000000000000000000..5f35fe3479691ac0cfc2022a4ddec63726c9b835 --- /dev/null +++ b/chap-16-interfaces/16-interfaces.md @@ -0,0 +1,736 @@ +# 第十六章:接口 + +![](imgs/interfaces.bef38e70.jpg) + +## 1 你将在本章学到什么 + +- 什么是接口? +- 如何定义接口。 +- “实现一个接口”是什么意思。 +- 接口的优点。 + +## 2 涵盖的技术概念 + +- 接口 +- 具体实现 +- 实现一个接口 +- 接口的方法集 + +## 3 介绍 +刚开始编程时,接口似乎很难理解。 新程序员通常不能完全理解接口的潜力。 本节旨在解释什么是接口,为什么它有趣,以及如何创建接口。 + +## 4 接口的基本定义 + +- 一个接口就是定义一组行为的**契约**。 +- 接口是**一个纯粹的设计对象**,它只是定义了一组**没有给出任何实现**的行为(方法)。 +- 一个接口就是**一种类型**,它定义了一组方法而不实现它们 + +“实现”=“编写方法的代码”。这是一个示例接口类型(来自标准包 `io` ): + +``` +type Reader interface { + Read(p []byte) (n int, err error) +} +``` + +这里我们有一个名为 `Reader` 的接口类型。 它指定了一种名为 `Read` 的方法。 该方法没有主体,没有实现。 唯一指定的是方法名称及其签名(参数和结果)。 + +### 4.0.0.1 接口类型的零值 +接口类型的零值为 `nil`。 例子: + +``` +var r io.Reader +log.Println(r) +// 2021/02/02 20:27:52 +``` + +## 5 基本示例 + +``` +type Human struct { + Firstname string + Lastname string + Age int + Country string +} + +type DomesticAnimal interface { + ReceiveAffection(from Human) + GiveAffection(to Human) +} +``` + +- 首先,我们声明一个名为 `Human` 的类型 +- 我们声明了一个名为 `DomesticAnimal` 的新类型接口。 +- 这种类型的接口有一个由两个方法组成的方法集:`ReceiveAffection` 和 `GiveAffection`。 + +`DomesticAnimal` 是一种契约。 + +- 它向开发人员发出信号,要成为 `DomesticAnimal`,我们至少需要有两种行为:`ReceiveAffection` 和 `GiveAffection` + +让我们创建两种类型: + +``` +type Cat struct { + Name string +} + +type Dog struct { + Name string +} +``` + +我们有两种新类型。 为了让它们**遵守**我们的接口 `DomesticAnimal` 的**约定**,我们必须为每种类型定义接口指定的方法。 + +让我们从 `Cat` 类型开始: + +``` +func (c Cat) ReceiveAffection(from Human) { + fmt.Printf("The cat named %s has received affection from Human named %s\n", c.Name, from.Firstname) +} + +func (c Cat) GiveAffection(to Human) { + fmt.Printf("The cat named %s has given affection to Human named %s\n", c.Name, to.Firstname) +} +``` + +现在 `Cat` 类型实现了 `DomesticAnimal` **接口**。 我们现在对 `Dog` 类型做同样的事情: + +``` +func (d Dog) ReceiveAffection(from Human) { + fmt.Printf("The dog named %s has received affection from Human named %s\n", d.Name, from.Firstname) +} + +func (d Dog) GiveAffection(to Human) { + fmt.Printf("The dog named %s has given affection to Human named %s\n", d.Name, to.Firstname) +} +``` + +我们的 `Dog` 类型现在正确地实现了 `DomesticAnimal` **接口**。 现在我们可以创建一个函数,它接受一个带有参数的接口: + +``` +func Pet(animal DomesticAnimal, human Human) { + animal.GiveAffection(human) + animal.ReceiveAffection(human) +} +``` + +该函数可以采用任何实现了 `DomesticAnimal` 类型接口的类型作为参数。 因此,我们不必为猫、狗、蛇、老鼠创建特定的函数……这个函数是通用的,可以为不同的类型执行: + +``` +func Pet(animal DomesticAnimal, human Human) { + animal.GiveAffection(human) + animal.ReceiveAffection(human) +} +``` + +`Pet` 函数将一个 `DomesticAnimal` 类型的接口作为第一个参数,一个 `Human` 作为第二个参数。 + +在函数内部,我们调用了接口的两个函数。 + +让我们使用这个函数: + +``` +// interfaces/first-example/main.go +//... + +func main() { + + // Create the Human + var john Human + john.Firstname = "John" + + + // Create a Cat + var c Cat + c.Name = "Maru" + + // then a dog + var d Dog + d.Name = "Medor" + + Pet(c, john) + Pet(d,john) +} +``` + +- `Dog` 和 `Cat` 类型实现了**接口** `DomesticAnimal` 的方法 +- => 任何类型为 `Dog` 和 `Cat` 的变量都**可以看作**是 `DomesticAnimal` + +## 6 编译器在看着你! +遵守类型 **T** 的接口契约意味着实现接口的所有方法。 让我们试着欺骗编译器看看会发生什么: + +``` +// ... +// 让我们创建一个具体的类型 Snake +type Snake struct { + Name string +} +// 我们没有故意实现 ReceiveAffect 和 GiveAffect 方法 +//... + + +func main(){ + + var snake Snake + snake.Name = "Joe" + + Pet(snake, john) +} +``` + +- 我们创建了一个新类型的 `Snake` +- 该类型没有实现 `DomesticAnimal` 动物的任何方法 +- 在主函数中,我们创建了一个 `Snake` 类型的新变量 +- 然后我们用这个变量作为第一个参数调用 `Pet` 函数。 + +编译时报错: + +``` +./main.go:70:5: cannot use snake (type Snake) as type DomesticAnimal in argument to Pet: + Snake does not implement DomesticAnimal (missing GiveAffection method) +``` + +## 7 示例:database/sql/driver.Driver +我们来看看 `Driver` 接口(来自包 `database/sql/driver` ) + +``` +type Driver interface { + Open(name string) (Conn, error) +} +``` + +- 存在不同种类的 SQL 数据库,因此 `Open` 方法有多种实现。 +- 为什么? 因为您不会使用相同的代码来启动到 MySQL 数据库和 Oracle 数据库的连接。 +- 通过构建接口,您定义了一个可供多个实现使用的契约。 + +## 8 接口嵌入 +您可以将接口**嵌入**到其他接口中。 让我们举个例子: + +``` +// 标准库中的 Stringer 类型接口 +type Stringer interface { + String() string +} +// 一个自定义的接口 +type DomesticAnimal interface { + ReceiveAffection(from Human) + GiveAffection(to Human) + // 将接口Stringer嵌入到DomesticAnimal接口中 + Stringer +} +``` + +在前面的清单中,我们将接口 `Stringer` 嵌入到接口 `DomesticAnimal` 中。 + +**因此**,已经实现了 `DomesticAnimal` 的其他类型必须实现 `DomesticAnimal` 接口的方法。 + +- 通过接口嵌入,您可以在不重复的情况下向接口添加功能。 + +- 这也是有代价的。 如果您从另一个模块嵌入一个接口,您的代码将与其耦合 + + - 其他模块接口的更改将迫使您重写代码。 + + - 请注意,如果依赖模块遵循语义版本控制方案,则这种危险会得到缓和 1 + + - 您可以放心使用标准库中的接口 +## 9 来自标准库的一些有用的(和著名的)接口 +### 9.1 错误接口 + +``` +type error interface { + Error() string +} +``` + +这种接口类型被大量使用。 可能失败的函数或方法返回 `error` 类型接口: + +``` +func (c *Communicator) SendEmailAsynchronously(email *Email) error { + //... +} +``` + +要创建错误,我们通常调用: `fmt.Errorf()` 返回类型 `error` 的结果 + +- 或 `errors.New()` + +当然,可以创建实现错误接口的类型。 +### 9.2 fmt.Stringer 接口 + +``` +type Stringer interface { + String() string +} +``` + +使用 `Stringer` **接口**,您可以定义在调用打印方法时如何将类型打印为 **string**( `fmt.Errorf()` , `fmt.Println` , `fmt.Printf` , `fmt.Sprintf` ...) + +这是一个示例实现 + +``` +type Human struct { + Firstname string + Lastname string + Age int + Country string +} + +func (h Human) String() string { + return fmt.Sprintf("human named %s %s of age %d living in %s",h.Firstname,h.Lastname,h.Age,h.Country) +} +``` + +`Human` 实现了 `Stringer`: + +``` +// interfaces/stringer/main.go +package main + +func main() { + var john Human + john.Firstname = "John" + john.Lastname = "Doe" + john.Country = "USA" + john.Age = 45 + + fmt.Println(john) +} +``` + +输出: + +``` +human named John Doe of age 45 living in the USA +``` + +### 9.3 sort.Interface +通过在一个类型上实现这个接口,你可以对一个类型的元素进行排序(通常,底层类型是一个切片) + +``` +type Interface interface { + Len() int + Less(i, j int) bool + Swap(i, j int) +} +``` + +这是一个示例用法(文件:sort/example_interface_test.go): + +``` +type Person struct { + Age int +} +// ByAge 根据 Age 字段为 []Person 实现 sort.Interface。 +type ByAge []Person + +func (a ByAge) Len() int { return len(a) } +func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } +``` + +- `ByAge` 类型实现了 `sort.Interface`。 + - 底层类型是 `Person` 的一个切片 +- 该接口由三个方法组成: + - `Len() int` : 应该返回集合内元素的数量 + - `Less(i, j int) bool` : 如果索引 `i` 处的元素应该排在索引处 `j` 处的元素之前,则返回 **`true`** + - `Swap(i, j int)` :应该交换索引 `i` & `j` 处的元素; 换句话说,我们应该将位于索引 `j` 的元素放在索引 `i` 处,而位于索引 `i` 的元素应该放在索引 `j` 处。 + +然后我们可以使用 `sort.Sort` 函数对 `ByAge` 类型的变量进行排序 + +``` +// interfaces/sort/main.go + +func main() { + people := []Person{ + {"Bob", 31}, + {"John", 42}, + {"Michael", 17}, + {"Jenny", 26}, + } + + sort.Sort(ByAge(people)) +} +``` + +## 10 隐式实现 +接口是**隐式**实现的。 当您声明一个类型时,您不必指定它实现了哪些接口。 +## 11 PHP and JAVA +在其他语言中,您必须指定接口实现。 + +这是 Java 中的一个示例: + +``` +// JAVA +public class Cat implements DomesticAnimal{ + public void receiveAffection(){ + //... + } + public void giveAffection(){ + //.. + } +} +``` + +这是 PHP 中的另一个示例 + +``` +//PHP + +``` + +在这两种语言中,我们有课程。 您可以看到,在声明实现接口的类时,必须添加术语 “**implements**”。 + +您可能会问 Go 运行时如何处理这些隐式接口实现。我们将尝试解释接口值的机制。 + +## 12 空接口 +Go 的空接口是您可以编写的最简单、更小的接口。 它的方法集正好由 0 个方法组成。 + +``` +interface {} +``` + +话虽如此,每种类型都实现了空接口。 你可能会问为什么这么无聊的界面很有趣。 根据定义,空接口值可以保存任何类型的值。 如果你想构建一个接受任何类型的方法,它会很有用。 + +让我们从标准库中举一些例子。 + +- 在 `log` 包中,您有一个 `Fatal` 方法,可以将任何类型的输入变量作为输入: + +``` +func (l *Logger) Fatal(v ...interface{}) { } +``` + +在 `fmt` 包中,我们还有许多方法将空接口作为输入。 例如 `Printf` 函数: + +``` +func Printf(format string, a ...interface{}) (n int, err error) { } +``` + +### 12.1 类型开关 +接受空接口作为参数的函数通常需要知道其输入参数的有效类型。 + +为此,该函数可以使用“类型开关”,这是一个比较类型而不是值的开关案例。 + +这是从标准库(文件:`runtime/error.go`,包运行时)中获取的示例: + +``` +// printany 打印传递给 panic 的参数。 +// 如果使用具有 String 或 Error 方法的值调用 panic, +// 它已经被 preprintpanics 转换成字符串。 +func printany(i interface{}) { + switch v := i.(type) { + case nil: + print("nil") + case bool: + print(v) + case int: + print(v) + case int8: + print(v) + case int16: + print(v) + case int32: + print(v) + case int64: + print(v) + case uint: + print(v) + case uint8: + print(v) + case uint16: + print(v) + case uint32: + print(v) + case uint64: + print(v) + case uintptr: + print(v) + case float32: + print(v) + case float64: + print(v) + case complex64: + print(v) + case complex128: + print(v) + case string: + print(v) + default: + printanycustomtype(i) + } +} +``` + +![](imgs/switch_cases_syntax.70671719.png) +类型开关语法 + +### 12.2 关于空接口的使用 +- 您应该非常小心地使用空接口。 +- 当您别无选择时,请使用空接口。 +- 空接口**不会**向将使用您的函数或方法的人**提供任何信息**,因此他们将不得不参考文档,这可能会令人沮丧。 +- 如果您接受一个空接口,您的函数/方法将不得不检查输入类型,从而使代码更加复杂。 + +你更喜欢哪种方法? + +``` +func (c Cart) ApplyCoupon(coupon Coupon) error { + //... +} + +func (c Cart) ApplyCoupon2(coupon interface{}) (interface{},interface{}) { + //... +} +``` + +`ApplyCoupon` 方法严格指定它将接受和返回的类型。 而 `ApplyCoupon2` 没有在输入和输出中指定它的类型。 作为调用方,使用 `ApplyCoupon2` 比 `ApplyCoupon` 更难。 + +## 13 应用:购物车存储 +### 13.1 规格 +您建立了一个电子商务网站; 您必须存储和查询客户购物车。 必然存在以下两种行为: + 1. 通过 ID 获取购物车 + 2. 将购物车放入数据库 + +为这两种行为提出一个接口。 + +还创建一个实现这两个接口的类型。 (不要实现方法中的逻辑。) + +### 13.2 解决方案 +这是一个建议的接口: + +``` +// interfaces/application/main.go + +type CartStore interface { + GetById(ID string) (*cart.Cart, error) + Put(cart *cart.Cart) (*cart.Cart, error) +} +``` + +实现接口的类型: + +``` +type CartStoreMySQL struct{} + +func (c *CartStoreMySQL) GetById(ID string) (*cart.Cart, error) { + // implement me +} + +func (c *CartStoreMySQL) Put(cart *cart.Cart) (*cart.Cart, error) { + // implement me +} +``` + +另一种实现接口的类型: + +``` +type CartStorePostgres struct{} + +func (c *CartStorePostgres) GetById(ID string) (*cart.Cart, error) { + // implement me +} + +func (c *CartStorePostgres) Put(cart *cart.Cart) (*cart.Cart, error) { + // implement me +} +``` + +- 您可以为您使用的每个数据库模型创建一个特定的实现 +- 添加对新数据库引擎的支持很容易! 你只需要创建一个实现接口的新类型。 + +## 14 为什么要使用接口? +### 14.1 进化性 +当您在方法或函数中使用接口作为输入时,您将程序设计为具有进化性。未来的开发人员(或未来的您)可以在不更改大部分代码的情况下创建新的实现。 + +假设您构建了一个执行数据库读取、插入和更新的应用程序。您可以使用两种设计方法: +1. 创建与您现在使用的数据库引擎密切相关的类型和方法。 +2. 创建一个接口,列出数据库引擎的所有操作和具体实现。 +- 在第一种方法中,您创建将特定实现作为参数的方法。 +- 通过这样做,您将程序锁定到一个实现 +- 在第二种方法中,您创建接受接口的方法。 +- 改变实现就像创建一个实现接口的新类型一样简单 + +### 14.2 提高团队合作 +团队也可以从接口中受益。 + +在构建功能时,通常需要多个开发人员来完成这项工作。如果工作需要两个团队编写的代码进行交互,他们可以就一个或多个接口达成一致。 + +然后,两组开发人员可以处理他们的代码并使用商定的接口。他们甚至可以嘲笑其他团队的工作。通过这样做,团队不会被对方阻止。 + +### 14.3 从一组例程中受益 +当您在自定义类型上实现接口时,您可以使用不需要开发的附加功能。让我们从标准库中举一个例子:`sort` 包。这并不奇怪。这个包是用来...排序的东西。这是 go 源代码的摘录: + +``` +// go v.1.10.1 +package sort +//.. + +type Interface interface { + // Len 是集合中元素的数量。 + Len() int + // Less 报告索引为 i 的元素是否应该排在索引为 j 的元素之前。 + Less(i, j int) bool + // Swap 交换索引为 i 和 j 的元素。 + Swap(i, j int) +} + +// 对数据进行排序。 +// 它对 data.Len 进行一次调用以确定 n,并且 O(n*log(n)) 调用 data.Less 和 data.Swap。 +// 这种排序不能保证是稳定的。 +func Sort(data Interface) { + n := data.Len() + quickSort(data, 0, n, maxDepth(n)) +} +``` + +在第一行,我们声明当前包:`sort` 。 在接下来的几行中,程序员声明了一个名为 `Interface` 的接口。 这个接口 `Interface` 指定了三个方法:`Len、Less、Swap`。 + +在接下来的几行中,函数 `Sort` 被声明。 它将 `Interface` 类型的参数 `data` 作为参数。 这是一个非常有用的函数,可以对给定的数据进行排序。 + +我们如何在我们的一种类型上使用这个函数? 实现接口: + +想象一下,你有一个 `User` 类型: + +``` +type User struct { + firstname string + lastname string + totalTurnover float64 +} +``` + +还有一个类型 `Users` ,它是 `User` 实例的一部分: + +``` +type Users []User +``` + +让我们创建一个 `Users` 实例并用三个 `User` 类型的变量填充它: + +``` +user0 := User{firstname:"John", lastname:"Doe", totalTurnover:1000} +user1 := User{firstname:"Dany", lastname:"Boyu", totalTurnover:20000} +user2 := User{firstname:"Elisa", lastname:"Smith Brown", totalTurnover:70} + +users := make([]Users,3) +users[0] = user0 +users[1] = user1 +users[2] = user2 +``` + +如果我们想按营业额排序怎么办? 我们可以从头开始开发符合我们规范的排序算法。 或者我们可以只实现使用 `sort` 包中的内置函数 `Sort` 所需的接口。 我们开始做吧: + +``` +// 计算数组的长度。 简单的... +func (users Users) Len() int { + return len(users) +} + +// 决定哪个实例比另一个大 +func (users Users) Less(i, j int) bool { + return users[i].totalTurnover < users[j].totalTurnover +} + +// 交换数组的两个元素 +func (users Users) Swap(i, j int) { + users[i], users[j] = users[j], users[i] +} +``` + +通过声明这些函数,我们可以简单地使用 `Sort` 函数: + +``` +sort.Sort(users) +fmt.Println(users) +// 将会输出 : +[{Elisa Smith Brown 70} {John Doe 1000} {Dany Boyu 20000}] +``` + +## 15 一点建议 +1. 使用标准库提供的接口 +2. 方法太多的接口很难实现(因为它需要编写很多方法)。 + +## 16 小练习 +### 16.1 问题 +1. 举一个接口嵌入另一个接口的例子。 +2. 嵌入接口中指定的方法不是接口方法集的一部分,对或错? +3. 说出使用接口的两个优点。 +4. 接口类型的零值是多少? + +### 16.2 答案 +1. 举一个接口嵌入另一个接口的例子。 + +``` +type ReadWriter interface { + Reader + Writer +} +``` + +1. 嵌入接口中指定的方法不是接口方法集的一部分,对或错? + 1. 错误的 + 2. 接口的方法集由以下部分组成: + 1. 直接指定到接口中的方法 + 2. 来自嵌入式接口的方法。 +2. 说出在代码中使用接口的两个优点。 + 1. 轻松地在开发人员之间拆分工作: + 1. 定义接口类型 + 2. 一个人开发接口的实现 + 3. 另一个人可以在其功能中使用接口类型 + 4. 两个人可以互不干扰地工作。 + 2. 进化性 + 1. 当你创建一个接口时,你就创建了一个契约。 + 2. 不同的实现可以履行这个契约。 + 3. 在一个项目的开始,通常有一个实现 + 4. 但随着时间的推移,可能需要另一种实现方式。 +3. 接口类型的零值是多少? +`nil` + +## 17个关键要点 +- 接口就是**契约** +- 它指定方法(行为)而不实现它们。 + +``` +type Cart interface { + GetById(ID string) (*cart.Cart, error) + Put(cart *cart.Cart) (*cart.Cart, error) +} +``` + +- 接口是一种类型(如结构、数组、映射等) +- 我们将接口中指定的方法称为接口的方法集 +- 一个类型可以实现多个接口。 +- 无需明确类型实现接口 + - 与其他需要声明它的语言(PHP、Java 等)相反 +- 一个接口可能嵌入到另一个接口中; 在这种情况下,嵌入的接口方法被添加到接口中。 +- 接口类型可以像任何其他类型一样使用 +- 接口类型的零值为 `nil` 。 +- 任何类型实现空接口 **`interface{}`** +- 空接口指定0个方法 +- 要获取空接口的具体类型,您可以使用类型开关: + +``` +switch v := i.(type) { + case nil: + print("nil") + case bool: + print(v) + case int: + print(v) +} +``` + +- 当我们可以通过各种方式实现一个行为时,我们或许可以创建一个接口。 + - 例如:存储(我们可以使用 MySQL、Postgres、DynamoDB、Redis 数据库来存储相同的数据) + +**** +1. 有关语义版本控制的更多信息,请参阅 Go 模块章节。 \ No newline at end of file diff --git a/chap-16-interfaces/imgs/interfaces.bef38e70.jpg b/chap-16-interfaces/imgs/interfaces.bef38e70.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e35936a80e6530e1117fa54224d2063fdc421d8 Binary files /dev/null and b/chap-16-interfaces/imgs/interfaces.bef38e70.jpg differ diff --git a/chap-16-interfaces/imgs/switch_cases_syntax.70671719.png b/chap-16-interfaces/imgs/switch_cases_syntax.70671719.png new file mode 100644 index 0000000000000000000000000000000000000000..88aa45f08cab18beeb56bb4e7a9b00e9026bff8c Binary files /dev/null and b/chap-16-interfaces/imgs/switch_cases_syntax.70671719.png differ