第十六章:接口
本篇翻译自《Practical Go Lessons》 Chapter 16: Interfaces
1 你将在本章学到什么?
- 什么是类型接口?
- 如何定义接口。
- “实现一个接口”是什么意思?
- 接口的优点
2 涵盖的技术概念
- 接口 interface
- 具体实现 concrete implementation
- 实现一个接口
- 接口的方法集
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/11/28 12:27:52 <nil>
5 基本示例
type Human struct { Firstname string Lastname string Age int Country string } type DomesticAnimal interface { ReceiveAffection(from Human) GiveAffection(to Human) }
- 首先,我们声明一个名为
Human
的类型 - 我们声明了一个名为
DomesticAnimal
的新类型接口 - 这种类型的接口有一个由两个方法组成的方法集:
ReceiveAffection
和GiveAffect
。
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) }
Pet函数将
DomesticAnimal类型的接口作为第一个参数,将
Human作为第二个参数。
在函数内部,我们调用了接口的两个函数。
让我们使用这个函数:
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
只要
Cat实现的方法的函数签名与接口定义一致就可以,不强制要求完全相同变量名和返回名。所以我们将函数func (c Cat) ReceiveAffection(from Human) {...}改成func (c Cat) ReceiveAffection(f Human) {...}也是可以的
6 编译器在看着你!
遵守类型 T 的接口契约意味着实现接口的所有方法。让我们试着欺骗编译器看看会发生什么:
// ... // let's create a concrete type Snake type Snake struct { Name string } // we do not implement the methods ReceiveAffection and GiveAffection intentionally //... 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 接口嵌入
你可以将接口嵌入到其他接口中。让我们举个例子:
// the Stringer type interface from the standard library type Stringer interface { String() string }// A homemade interface type DomesticAnimal interface { ReceiveAffection(from Human) GiveAffection(to Human) // embed the interface Stringer into the DomesticAnimal interface Stringer }
在上面的代码中,我们将接口
Stringer嵌入到接口
DomesticAnimal中。 因此,已经实现了
DomesticAnimal的其他类型必须实现
Stringer接口的方法。
- 通过接口嵌入,你可以在不重复的情况下向接口添加功能。
- 这也是有代价的,如果你从另一个模块嵌入一个接口,你的代码将与其耦合 其他模块接口的更改将迫使你重写代码。
- 请注意,如果依赖模块遵循语义版本控制方案,则这种危险会得到缓和
- 你可以毫无畏惧地使用标准库中的接口
9 来自标准库的一些有用(和著名)的接口
9.1 Error 接口
type error interface { Error() string }
这个接口类型被大量使用,用于当函数或方法执行失败是返会
error类型接口:
func (c *Communicator) SendEmailAsynchronously(email *Email) error { //... }
要创建一个 error ,我们通常调用:
fmt.Errorf()返回一个
error类型的结果,或者使用
errors.New()函数。 当然,你也可以创建实现
error接口的类型。
9.2 fmt.Stringer 接口
type Stringer interface { String() string }
使用
Stringer接口,你可以定义在调用打印方法时如何将类型打印为字符串(
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接口:
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.Interface接口,可以对一个类型的元素进行排序(通常,底层类型是一个切片)。
这是一个示例用法(来源:sort/example_interface_test.go):
type Person struct { Age int } // ByAge implements sort.Interface for []Person based on // the Age field. 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类型的变量进行排序
func main() { people := []Person{ {"Bob", 31}, {"John", 42}, {"Michael", 17}, {"Jenny", 26}, } sort.Sort(ByAge(people)) }
10 隐式实现
接口是隐式实现的。当你声明一个类型时,你不必指定它实现了哪些接口。
11 PHP 和 JAVA
在其他语言中,你必须指定接口实现。 这是 Java 中的一个示例:
// JAVA public class Cat implements DomesticAnimal{ public void receiveAffection(){ //... } public void giveAffection(){ //.. } }
这是 PHP 中的另一个示例:
//PHP <?php class Cat implements DomesticAnimal { public function receiveAffection():void { // ... } public function giveAffection():void { // ... } } ?>
你可以看到,在声明实现接口的类时,必须添加关键字
"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 类型转换
接受空接口作为参数的函数通常需要知道其输入参数的有效类型。 为此,该函数可以使用“类型开关”,这是一个 switch case 将比较类型而不是值。 这是从标准库(文件
runtime/error.go,包
runtime)中获取的示例:
// printany prints an argument passed to panic. // If panic is called with a value that has a String or Error method, // it has already been converted into a string by 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) } }
12.2 关于空接口的使用
- 你应该非常小心地使用空接口。
- 当你别无选择时,请使用空接口。
- 空接口不会向将使用你的函数或方法的人提供任何信息,因此他们将不得不参考文档,这可能会令人沮丧。
你更喜欢哪种方法?
func (c Cart) ApplyCoupon(coupon Coupon) error { //... } func (c Cart) ApplyCoupon2(coupon interface{}) (interface{},interface{}) { //... }
ApplyCoupon方法严格指定它将接受和返回的类型。而
ApplyCoupon2没有在输入和输出中指定它的类型。作为调用方,
ApplyCoupon2的使用难度比
ApplyCoupon大。
13 实际应用:购物车存储
13.1 规则说明
你建立了一个电子商务网站;你必须存储和检索客户购物车。必须支持以下两种行为:
- 通过 ID 获取购物车
- 将购物车数据放入数据库
为这两种行为提出一个接口。还要创建一个实现这两个接口的类型(不要实现方法中的逻辑)。
13.2 答案
这是一个设计的接口:
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 易于升级
当你在方法或函数中使用接口作为输入时,你将程序设计为易于升级的。未来的开发人员(或未来的你)可以在不更改大部分代码的情况下创建新的实现。
假设你构建了一个执行数据库读取、插入和更新的应用程序。你可以使用两种设计方法:
- 创建与你现在使用的数据库引擎密切相关的类型和方法。
- 创建一个接口,列出数据库引擎的所有操作和具体实现。
- 在第一种方法中,你创建将特定实现作为参数的方法。
- 通过这样做,你将程序限制到一个实现。
- 在第二种方法中,你创建接受接口的方法。
- 改变实现就像创建一个实现接口的新类型一样简单。
14.2 提高团队合作
团队也可以从接口中受益。 在构建功能时,通常需要多个开发人员来完成这项工作。如果工作需要两个团队编写的代码进行交互,他们可以就一个或多个接口达成一致。 然后,两组开发人员可以处理他们的代码并使用商定的接口。他们甚至可以 mock 其他团队的返回结果。通过这样做,团队不会被阻塞。
14.3 Benefit from a set of routines
在自定义类型上实现接口时,你可以不需要开发就使用的附加功能。让我们从标准库中举一个例子:
sort包。这并不奇怪。这个包是用来进行排序的。这是
go源代码的摘录:
// go v.1.10.1 package sort //.. type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) } // Sort sorts data. // It makes one call to data.Len to determine n, and O(n*log(n)) calls to // data.Less and data.Swap. The sort is not guaranteed to be stable. func Sort(data Interface) { n := data.Len() quickSort(data, 0, n, maxDepth(n)) }
在第一行,我们声明当前包:
sort。在接下来的几行中,程序员声明了一个名为
Interface的接口。这个接口
Interface指定了三个方法:
Len、Less、Swap。
在接下来的几行中,函数
Sort被声明。它将接口类型
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 所需的接口。我们开始吧:
// Compute the length of the array. Easy... func (users Users) Len() int { return len(users) } // decide which instance is bigger than the other one func (users Users) Less(i, j int) bool { return users[i].totalTurnover < users[j].totalTurnover } // swap two elements of the array func (users Users) Swap(i, j int) { users[i], users[j] = users[j], users[i] }
通过声明这些函数,我们可以简单地使用
Sort函数:
sort.Sort(users) fmt.Println(users) // will output : [{Elisa Smith Brown 70} {John Doe 1000} {Dany Boyu 20000}]
15 一点建议
- 尽量使用标准库提供的接口
- 方法太多的接口很难实现(因为它需要编写很多方法)。
16 随堂测试
16.1 问题
- 举一个接口嵌入另一个接口的例子。
- 判断真假。嵌入接口中指定的方法不是接口方法集的一部分。
- 说出使用接口的两个优点。
- 接口类型的零值是多少?
16.2 答案
- 举一个接口嵌入另一个接口的例子。
type ReadWriter interface { Reader Writer }
- 判断真假。嵌入接口中指定的方法不是接口方法集的一部分。 错。接口的方法集是由两个部分组成: [ol] 直接指定到接口中的方法
- 来自嵌入接口的方法
-
轻松地在开发人员之间拆分工作:
1.定义接口类型
2.一个人开发接口的实现
3.另一个人可以在其功能中使用接口类型
4.两个人可以互不干扰地工作。
nil
17 关键要点
- 接口就是契约
- 它指定方法(行为)而不实现它们。
type Cart interface { GetById(ID string) (*cart.Cart, error) Put(cart *cart.Cart) (*cart.Cart, error) }
- 接口是一种类型(就像structs, arrays, maps,等)
- 我们将接口中指定的方法称为接口的方法集。
- 一个类型可以实现多个接口。
- 无需明确类型实现了哪个接口 与其他需要声明它的语言(PHP、Java 等)相反
nil。
interface{}
switch v := i.(type) { case nil: print("nil") case bool: print(v) case int: print(v) }
- 当我们可以通过各种方式实现一个行为时,我们或许可以创建一个接口。 例如:存储(我们可以使用 MySQL、Postgres、DynamoDB、Redis 数据库来存储相同的数据)
- 第十六章:Android LCD(二):LCD常用接口原理篇
- ATL接口映射宏详解
- wince流接口驱动工作原理
- caffe中python接口配置实践
- Java开发WebService接口记录
- 【JAVA】使用集合容器Set等,模拟学生选课功能(Student类和Course类),涵盖自比较接口和外部比较器接口的使用~
- 【16位微机原理、汇编语言及接口技术教程(钱晓捷)】8088/8086的寄存器结构与存储器结构
- 接口可能存在性能问题
- IEnumerable接口与IEnumerator
- ASP.NET MVC Model元数据及其定制:一个重要的接口IMetadataAware
- CharSequence接口在方法定义中的应用
- jvm原理(4)接口初始化规则与类加载器准备阶段和初始化阶段的重要意义
- spring中基础核心接口介绍
- java集合类之集合类接口实现的类
- MyBatis 源码分析——SqlSession接口和Executor类
- Yii 扩展支付宝快速支付接口
- android中接口回调的理解
- java接口
- 接口和抽象类有什么区别
- 尽量使用接口来编程等基本技巧