0%

Go语言学习笔记6-接口

[TOC]

接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。

Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。

非侵入式设计是 Go 语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低不少。

Go语言接口声明定义

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口声明的格式

每个接口类型由数个方法组成,接口的形式代码如下:

1
2
3
4
5
type 接口类型名 interface {
方法名1(参数列表1) 返回值列表1
方法名2(参数列表2) 返回值列表2
...
}

对各个部分的说明:

  • 接口类型名: 使用 type 将接口定义为自定义的类型名。Go 语言在接口命名时,一并会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer…

  • 方法名: 当方法名首字母大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问

  • 参数列表、返回值列表: 参数列表和返回值列表中的参数变量名可以被忽略,如

    1
    2
    3
    type write interface {
    Write([]byte) error
    }

开发中常见接口及写法

Go 语言提供的很多包中都有接口,例如 io 包中提供的Writer 接口:

1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)

Go语言实现接口的条件

接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用

条件一:接口的方法与实现接口的类型方式格式一致

在类型中添加与接口签名一致的方法就可以实现该接口。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。

数据写入器的抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "fmt"

// 定义一个数据写入器
type DataWriter interface {
// 定义 DataWriter 接口。这个接口只有一个方法,即 WriteData(),入参接收interface{} 类型的 data,返回一个 error 结构表示可能发生的错误
WriteData(data interface{}) error
}

// 定文件结构,用于实现DataWriter
type file struct {
}

// 实现DataWriter接口的WriteData方法
// file 的 WriteData() 方法使用指针接收器。输入一个 interface{} 类型的 data,返回 error。
func (f *file) WriteData(data interface{}) error {
fmt.Println("实现了DataWriter接口的WriteData方法" , data)
return nil
}

func main() {
// 实例化结构file
// 实例化 file 赋值给 f,f 的类型为 *file。
f := new(file)
// 声明一个DataWriter类型的writer接口变量
var writer DataWriter
// 将接口复赋值给f,也就是*file类型
// 将 *file 类型的 f 赋值给 DataWriter 接口的 writer,虽然两个变量类型不一致。但是 writer 是一个接口,且 f 已经完全实现了 DataWriter() 的所有方法,因此赋值是成功的。
writer = f
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}

当类型无法实现接口时,编译器会报错,下面列出常见的几种接口无法实现的错误。

1) 函数名不一致导致的报错

2) 实现接口的方法签名不一致导致的报错

条件二:接口中所有方法均被实现

当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。

在本节开头的代码中,为 DataWriter中 添加一个方法,代码如下:

1
2
3
4
5
6
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
// 能否写入
CanWrite() bool
}

新增 CanWrite() 方法,返回 bool。此时再次编译代码,报错:

1
2
cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (missing CanWrite method)

需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。

Go 语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。

实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。

Go语言类型与接口的关系

类型和接口之间有一对多和多对一的关系,下面将列举出这些常见的概念,以方便读者理解接口与类型在复杂环境下的实现关系。

一个类型可以实现多个接口

一个类型可以同时实现多个接口,而接口见彼此独立,不知道对方的实现

网络上的两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个 Socket。Socket 能够同时读取和写入数据,这个特性与文件类似。因此,开发中把文件和 Socket 都具备的读写特性抽象为独立的读写器概念。

Socket 和文件一样,在使用完毕后,也需要对资源进行释放。

把 Socket 能够写入数据和需要关闭的特性使用接口来描述,请参考下面的代码:

1
2
3
4
5
6
7
8
9
10
type Socket struct {
}
// Socket 结构的Write方法实现了io.Writer接口
func (s *Socket) Write(p []byte) (n int, err error) {
return 0, nil
}
// 同时,Socket 结构也实现了 io.Close 接口:
func (s *Socket) Close() error {
return nil
}

使用 Socket 实现的 Writer 接口的代码,无须了解 Writer 接口的实现者是否具备 Closer 接口的特性。同样,使用 Closer 接口的代码也并不知道 Socket 已经实现了 Writer 接口,如下图所示。

在代码中使用Socket结构实现的Writer接口和Closer接口代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Socket struct {
}

func (s *Socket) Write(p []byte) (n int, err error) {
return 0, nil
}
func (s *Socket) Close() error {
return nil
}

func usingWriter(writer io.Writer) {
writer.Write(nil)
}

func usingCloser(closer io.Closer) {
closer.Close()
}
func main() {
s := new(Socket)
usingWriter(s)
usingCloser(s)
}

多个类型可以实现相同的接口

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 一个服务需要满足能够开启和写日志的功能
type Service interface {
Start() // 开启服务
Log(string) // 日志输出
}
// 日志器 定义能输出日志的日志器结构。
type Logger struct {
}
// 为 Logger 添加 Log() 方法,同时实现 Service 的 Log() 方法。
func (g *Logger) Log(l string) {
}
// 定义 GameService 结构。
type GameService struct {
Logger // 在 GameService 中嵌入 Logger 日志器,以实现日志功能。
}
// 实现Service的Start()方法
func (g *GameService) Start() {
}

此时,实例化 GameService,并将实例赋给 Service,代码如下:

1
2
3
var s Service = new(GameService)
s.Start()
s.Log(“hello”)

s 就可以使用 Start() 方法和 Log() 方法,其中,Start() 由 GameService 实现,Log() 方法由 Logger 实现。

接口嵌套

Go语言中不同结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口

接口与接口嵌套组合而成了新接口,只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用

系统包中的接口嵌套组合

Go 语言的io 包中定义写入器(Writer)、关闭器(Closer)和写入关闭器(WriteClose)3个接口,代码如下:

1
2
3
4
5
6
7
8
9
10
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type WriterCloser interface {
Writer
Closer
}

在代码中使用接口嵌套组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type device struct {
}

func (d *device) Write(p []byte) (n int, err error) {
return 0, nil
}
func (d *device) Close() error {
return nil
}

func main() {
// 对 device 实例化,由于 device 实现了 io.WriteCloser 的所有嵌入接口,因此 device 指针就会被隐式转换为 io.WriteCloser 接口。
var wc io.WriteCloser = new(device)
// 写入数据
wc.Write(nil)

var writeOnly io.Writer = new(device)

writeOnly.Write(nil)
}

  1. io.WriteCloser的实现及调用过程如图 1 所示。

img

  1. io.Writer 的实现调用过程如图 2 所示。

img

给 io.WriteCloser 或 io.Writer 更换不同的实现者,可以动态地切换实现代码。