接口(interface)是一种抽象类型,它相较于那些具体的类型,更注重“我能做什么”的问题,它是一组方法的集合。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。

接口类型

每个接口类型由任意个方法签名组成,接口的定义格式如下:

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

其中:

  • 接口类型名:Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子,定义一个包含Write方法的Writer接口。

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

当你看到一个Writer接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事情。

接口实现的条件

接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。

我们定义的Singer接口类型,它包含一个Sing方法。

1
2
3
4
// Singer 接口
type Singer interface {
    Sing()
}

我们有一个Bird 结构体类型如下。

1
type Bird struct {}

因为Singer接口只包含一个Sing方法,所以只需要给Bird结构体添加一个Sing方法就可以满足Singer接口的要求。

1
2
3
4
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
	fmt.Println("汪汪汪")
}

这样就称为Bird实现了Singer接口。

为什么要实现接口

现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type Cat struct{}

func (c Cat) Say() {
	fmt.Println("喵喵喵~")
}

type Dog struct{}

func (d Dog) Say() {
	fmt.Println("汪汪汪~")
}

func main() {
	c := Cat{}
	c.Say()
	d := Dog{}
	d.Say()
}

这个时候又跑来了一只羊,羊饿了也会发出叫声。

1
2
3
4
5
type Sheep struct{}

func (s Sheep) Say() {
	fmt.Println("咩咩咩~")
}

我们接下来定义一个饿肚子的场景。

1
2
3
4
5
6
7
8
9
// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {
	c.Say()
}

// MakeSheepHungry 羊饿了会咩咩咩~
func MakeSheepHungry(s Sheep) {
	s.Say()
}

接下来会有越来越多的小动物跑过来,我们的代码世界该怎么拓展呢?

在饿肚子这个场景下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的Say()方法,这就足够了。

我们可以约定一个Sayer类型,它必须实现一个Say()方法,只要饿肚子了,我们就调用Say()方法。

1
2
3
type Sayer interface {
    Say()
}

然后我们定义一个通用的MakeHungry函数,接收Sayer类型的参数。

1
2
3
4
// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
	s.Say()
}

我们通过使用接口类型,把所有会叫的动物当成Sayer类型来处理,只要实现了Say()方法都能当成Sayer类型的变量来处理。

1
2
3
4
var c cat
MakeHungry(c)
var d dog
MakeHungry(d)

在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay方法让调用方调用就可以了。

再比如我们需要在某个程序中添加一个将某些指标数据向外输出的功能,根据不同的需求可能要将数据输出到终端、写入到文件或者通过网络连接发送出去。在这个场景下我们可以不关注最终输出的目的地是什么,只需要它能提供一个Write方法让我们把内容写入就可以了。

Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码。

值接收者和指针接收者

类型与接口的关系

接口组合

空接口