Go 第3.12章 Go语言基础-接口 Go 第3.12章 Go语言基础-接口

2021-06-09

在 Go 语言中接口(interface)是一种类型,一种抽象的类型。

相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。

接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在 Go 语言中提倡使用面向接口的编程方式实现解耦。

一、接口类型

接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。

相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么。

①、接口的定义

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

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

其中:

  • 接口类型名:Go 语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有关闭操作的接口叫 closer 等。接口名最好要能突出该接口的类型含义。

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

  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

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

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

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

②、实现接口的条件

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

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

// Singer 接口
type Singer interface {
   Sing()
}

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

type Bird struct {}

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

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

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

③、为什么要使用接口?

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

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()
}

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

type Sheep struct{}

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

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

// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {
   c.Say()
}

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

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

在饿肚子这个场景下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 

我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的 Say() 方法,这就足够了。

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

type Sayer interface {
   Say()
}

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

// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
   s.Say()
}

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

var c cat
MakeHungry(c)
var d dog
MakeHungry(d)

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

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

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

④、面向接口编程

PHP、Java 等语言中也有接口的概念,不过在 PHP 和 Java 语言中需要显式声明一个类实现了哪些接口,在 Go 语言中使用隐式声明的方式实现接口。

只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。

Go 语言中的这种设计符合程序开发中抽象的一般规律,例如在下面的代码示例中,我们的电商系统最开始只设计了支付宝一种支付方式:

type ZhiFuBao struct {
   // 支付宝
}

// Pay 支付宝的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
 fmt.Printf("使用支付宝付款:%.2f元。\n", float64(amount/100))
}

// Checkout 结账
func Checkout(obj *ZhiFuBao) {
   // 支付100元
   obj.Pay(100)
}

func main() {
   Checkout(&ZhiFuBao{})
}

随着业务的发展,根据用户需求添加支持微信支付。

type WeChat struct {
   // 微信
}

// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
   fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}

在实际的交易流程中,我们可以根据用户选择的支付方式来决定最终调用支付宝的 Pay 方法还是微信支付的 Pay 方法。

// Checkout 支付宝结账
func CheckoutWithZFB(obj *ZhiFuBao) {
   // 支付100元
   obj.Pay(100)
}

// Checkout 微信支付结账
func CheckoutWithWX(obj *WeChat) {
   // 支付100元
   obj.Pay(100)
}

实际上,从上面的代码示例中我们可以看出,我们其实并不怎么关心用户选择的是什么支付方式,我们只关心调用 Pay 方法时能否正常运行。

这就是典型的“不关心它是什么,只关心它能做什么”的场景。

在这种场景下我们可以将具体的支付方式抽象为一个名为 Payer 的接口类型,即任何实现了 Pay 方法的都可以称为 Payer 类型。

// Payer 包含支付方法的接口类型
type Payer interface {
   Pay(int64)
}

此时只需要修改下原始的 Checkout 函数,它接收一个 Payer 类型的参数。这样就能够在不修改既有函数调用的基础上,支持新的支付方式。

// Checkout 结账
func Checkout(obj Payer) {
   // 支付100元
   obj.Pay(100)
}

func main() {
   Checkout(&ZhiFuBao{}) // 之前调用支付宝支付

   Checkout(&WeChat{}) // 现在支持使用微信支付
}

像类似的例子在我们编程过程中会经常遇到:

  • 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?

  • 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?

  • 比如满减券、立减券、打折券都属于电商场景下常见的优惠方式,我们能不能把它们当成“优惠券”来处理呢?

接口类型是Go语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定,但是通常使用接口类型可以使代码更清晰易读。

⑤、接口类型变量

那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。

例如在上面的示例中,Dog 和 Cat 类型均实现了 Sayer 接口,此时一个 Sayer 类型的变量就能够接收 Cat 和 Dog 类型的变量。

var x Sayer // 声明一个Sayer类型的变量x
a := Cat{}  // 声明一个Cat类型变量a
b := Dog{}  // 声明一个Dog类型变量b
x = a       // 可以把Cat类型变量直接赋值给x
x.Say()     // 喵喵喵
x = b       // 可以把Dog类型变量直接赋值给x
x.Say()     // 汪汪汪

二、值接收者和指针接收者

在结构体那一章节中,我们介绍了在定义结构体方法时既可以使用值接收者也可以使用指针接收者。

那么对于实现接口来说使用值接收者和使用指针接收者有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们定义一个 Mover 接口,它包含一个 Move 方法。

// Mover 定义一个接口类型
type Mover interface {
   Move()
}

①、值接收者实现接口


阅读 2693