接口本身是调用方法和实现方法均需要遵守的一种协议,大家按照统一的方法命名参数和数量来协调逻辑处理函数的过程。
接口是双方约定的一种协议,接口实现者不需要关心接口会被怎样的使用,接口调用者也不需要关心接口的实现细节,接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口声明

接口声明格式

每个接口都由多个方法组成,接口的形式如下:

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

对各个部分的说明:

  • 接口类型名:使用type将接口定义为自定义的类型名,Go语言的接口在命名的时候,一般会在单词的后面添加er,如有写操作的接口可以写为Writer,有关闭操作的接口可以写为Closer等。
  • 方法名:当方法名首字母大写的时候,且这个接口类型名首字母也是大写,这个方法可以被接口所在包之外的代码所访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,比如:
1
2
3
type writer 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)。

实现接口条件

接口被定义之后,需要实现接口,调用方法才能正确编译通过并使用接口。

接口实现条件一

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

数据写入器的抽象过程:

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
34
35
36
package main 

import "fmt"

//定义一个文件结构,用于存放数据
type file struct {

}

//定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}

//实现DataWriter接口的WriteData方法
//这个实际上实现的是结构体的方法
func (d *file) WriteData(data interface{}) error {
//模拟数据写入
fmt.Println("Write data:", data)
return nil
}

func main() {
//实例化file
f := new(file)

//声明一个DataWriter的接口
var writer DataWriter

//将f赋值给接口,也就是*file类型
//因为file有WriteData的方法
writer = f

//使用DataWriter接口进行数据写入
writer.WriteData("data")
}

上述代码的调用及实现关系如图所示:
writeWriter的实现过程

接口实现条件二

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

Go语言的接口实现是隐式的,无需让实现接口的类型写出实现了哪些接口,这个设计就是非侵入式设计实现者在编写方法时,无法预测未来哪些方法会变成接口,一旦某个接口被创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这就会造成雪崩式的重新编译。
传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
对于 Go 语言来说,非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用 GO 语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。

类型与接口的关系

类型和接口之间有一对多和多对一的关系。

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

一个类型可以同时实现多个接口,并且接口之间彼此独立。
网络上的两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个 Socket。Socket 能够同时读取和写入数据,这个特性与文件类似。因此,开发中把文件和 Socket 都具备的读写特性抽象为独立的读写器概念。
Socket 和文件一样,在使用完毕后,也需要对资源进行释放。把 Socket 能够写入数据和需要关闭的特性使用接口来描述,请参考下面的代码:

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

}

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

func (s *Socket) Close() error {
return nil
}

//定义一个Writer接口
type Writer interface {
Write(p []byte) (n int, err error)
}

//定义一个Closer接口
type Closer interface {
Close() error
}

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

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

一个接口的方法不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其它类型或者结构体来实现。

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
//一个服务包含开启服务和日志记录的功能
type Service interface {
//开启服务
Strat()
//日志输出
Log(string)
}

//定义一个日志器
type Logger struct {}

//为Logger添加方法
func (g *Logger) Log(l string) {
}

//定义一个服务
type GameServer struct {
Logger //嵌入日志器
}

//为GameServer添加一个方法
func (g *GameServer) Start() error {
fmt.Println("Start...")
return nil
}

func main() {
var s Server = new(GameServer)
s.Start()
s.Log("Hello World")
}

Service 接口定义了两个方法:一个是开启服务的方法(Start()),一个是输出日志的方法(Log())。使用 GameService 结构体来实现 Service,GameService 自己的结构只能实现 Start() 方法,而 Service 接口中的 Log() 方法已经被一个能输出日志的日志器(Logger)实现了,无须再进行 GameService 封装,或者重新实现一遍。所以,选择将 Logger 嵌入到 GameService 能最大程度地避免代码冗余,简化代码结构。s 就可以使用 Start() 方法和 Log() 方法,其中,Start() 由 GameService 实现,Log() 方法由 Logger 实现。

接口的嵌套组合

系统包中的接口嵌套组合

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

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

type Closer interface {
Close() error
}

type WriteCloser interface {
Writer
Closer
}

WriteCloser这个接口由Writer和Closer两个接口嵌入,也就是说WriteCloser同时拥有了Write和Closer的特性。

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

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
package main 

import "io"

//声明一个设备结构
type device struct {}

//实现io.Writer的Write方法
func (d *device) Write(p []byte) (n int, err error) {
return 0, nil
}

//实现io.Closer的Close方法
func (d *divice) Close() error {
return nil
}

func main() {
//声明写入关闭器,并赋予device的实例
var wc io.WriteClose = new(device)

wc.Write(nil)
wc.Close()

//声明写入器,并赋予device的新实例
var writeOnly io.Writer = new(device)

writeOnly.Write(nil)
}

接口和类型之间的转换

Go语言中使用接口断言(type assertions)将接口转换成另一个接口,也可以将接口转换成为另外的类型,接口转换在开发中非常常见。

类型断言的格式

type assertions的基本格式:

1
t := i.(T)

其中i代表接口变量,T表示转换目标类型,t代表转换后的变量。如果i没有完全实现T接口的方法,这个语句就会触发宕机,触发宕机不是我们所希望看到的,所以上面的语句还有一种写法:

1
t, ok := i.(T)

这种写法如果发生接口没有实现的时,将会把ok设置为false,t设置为T类型的0值,正常实现的时候ok为true。

将接口转换成其它的接口

实现某个接口的类型同时实现了另外一个接口,此时可以在两个接口之间转换。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main 

import "fmt"

//定义飞行动物接口
type Flyer interface {
Fly()
}

//定义行走动物接口
type Walker interface {
Walk()
}

//定义鸟类
type bird struct {}

//实现飞行动物的接口
func (b *bird) Fly() {
fmt.Println("bird: fly")
}

//为鸟类添加Walk方法,实现行走动物的接口
func (b *bird) Walk() {
fmt.Prinln("bird: walk")
}

//定义一个行走的猪
type pig struct {}

//为猪添加行走的方法
func (p *pig) Walk() {
fmt.Println("pig: walk")
}

func main() {
//创建动物名称到实例映射
animals := map[string]interface{} {
"bird": new(bird),
"pig": new(pig),
}

//遍历映射
for name, obj := range animals {
//判断对象是不是飞行动物
f, isFlyer := obj.(Flyer)
//判断对象是不是行走动物
w, isWalker := obj.(Walker)

fmt.Printf("name: %s isFlyer: %v isWalker: %v\n", name, isFlyer, isWalker)
// 如果是飞行动物则调用飞行动物接口
if isFlyer {
f.Fly()
}
// 如果是行走动物则调用行走动物接口
if isWalker {
w.Walk()
}
}
}

总结

接口断言类似于流程控制中的if,单大量的断言出现的时候,应该使用更为高效的类型分支switch特性。

空接口类型 interface{}

空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

将值保存到空接口

空接口赋值如下所示:

1
2
3
4
5
6
7
8
9
10
var any interface{}

any = 1
fmt.Prinln(any)

any = "hello"
fmt.Println(any)

any = false
fmt.Println(any)

空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

从空接口中获取值

保存到空接口中的值,如果直接取出指定类型的值得时候,会发生编译错误:

1
2
3
var a int = 1
var i interface{} = a
var b int = i

程序编译到var b int = i的时候就会报错,原因是不能将i变量视为int类型赋值给b。为了使代码能够成功的运行。需要进行断言转换。

1
var b int = i.(int)

空接口的值比较

空接口在保存不同的值之后,可以和其它变量一样使用==进行比较操作,空接口比较有以下几种特性:

1.类型不同的空接口间的比较结果不相同
保存有类型不同的值的空接口进行比较的时候,Go语言会优先比较值得类型。

1
2
3
4
var a interface{} = 100
var b interface{} = "hi"

fmt.Println(a == b)

2.不能比较空接口中的动态值
当接口中保存有动态类型的值时候,运行将会触发错误:

1
2
3
4
var c interface{} = []int{10}
var d interface{} = []int{20}

fmt.Println(c == d) //会宕机

类型可比较性

类型 说明
map 宕机错误,不可比较
slice []T 宕机错误,不可比较
channel 可比较,必须有同一个make生成,也就是同一个通道才会是true,否则为false
数组 [容量]T 课比较,编译期知道两个数组是否一致
struct 可比较,可以逐个比较结构体的值
函数 可比较