[TOC]
接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。
Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。
非侵入式设计是 Go 语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低不少。
Go语言接口声明定义
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
接口声明的格式
每个接口类型由数个方法组成,接口的形式代码如下:
1 | type 接口类型名 interface { |
对各个部分的说明:
接口类型名: 使用 type 将接口定义为自定义的类型名。Go 语言在接口命名时,一并会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer…
方法名: 当方法名首字母大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问
参数列表、返回值列表: 参数列表和返回值列表中的参数变量名可以被忽略,如
1
2
3type write interface {
Write([]byte) error
}
开发中常见接口及写法
Go 语言提供的很多包中都有接口,例如 io 包中提供的Writer 接口:
1 | type Writer interface { |
这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)
Go语言实现接口的条件
接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用
条件一:接口的方法与实现接口的类型方式格式一致
在类型中添加与接口签名一致的方法就可以实现该接口。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
数据写入器的抽象:
1 | package main |
当类型无法实现接口时,编译器会报错,下面列出常见的几种接口无法实现的错误。
1) 函数名不一致导致的报错
2) 实现接口的方法签名不一致导致的报错
条件二:接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
在本节开头的代码中,为 DataWriter中 添加一个方法,代码如下:
1 | // 定义一个数据写入器 |
新增 CanWrite() 方法,返回 bool。此时再次编译代码,报错:
1 | cannot use f (type *file) as type DataWriter in assignment: |
需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。
Go 语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。
实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。
Go语言类型与接口的关系
类型和接口之间有一对多和多对一的关系,下面将列举出这些常见的概念,以方便读者理解接口与类型在复杂环境下的实现关系。
一个类型可以实现多个接口
一个类型可以同时实现多个接口,而接口见彼此独立,不知道对方的实现
网络上的两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个 Socket。Socket 能够同时读取和写入数据,这个特性与文件类似。因此,开发中把文件和 Socket 都具备的读写特性抽象为独立的读写器概念。
Socket 和文件一样,在使用完毕后,也需要对资源进行释放。
把 Socket 能够写入数据和需要关闭的特性使用接口来描述,请参考下面的代码:
1 | type Socket struct { |
使用 Socket 实现的 Writer 接口的代码,无须了解 Writer 接口的实现者是否具备 Closer 接口的特性。同样,使用 Closer 接口的代码也并不知道 Socket 已经实现了 Writer 接口,如下图所示。
在代码中使用Socket结构实现的Writer接口和Closer接口代码如下:
1 | type Socket struct { |
多个类型可以实现相同的接口
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。
1 | // 一个服务需要满足能够开启和写日志的功能 |
此时,实例化 GameService,并将实例赋给 Service,代码如下:
1 | var s Service = new(GameService) |
s 就可以使用 Start() 方法和 Log() 方法,其中,Start() 由 GameService 实现,Log() 方法由 Logger 实现。
接口嵌套
Go语言中不同结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口
接口与接口嵌套组合而成了新接口,只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用
系统包中的接口嵌套组合
Go 语言的io 包中定义写入器(Writer)、关闭器(Closer)和写入关闭器(WriteClose)3个接口,代码如下:
1 | type Writer interface { |
在代码中使用接口嵌套组合
1 | type device struct { |
- io.WriteCloser的实现及调用过程如图 1 所示。
- io.Writer 的实现调用过程如图 2 所示。
给 io.WriteCloser 或 io.Writer 更换不同的实现者,可以动态地切换实现代码。