首页 > 编程笔记 > Go语言笔记 阅读:11

Go语言接口用法详解(附带实例)

简而言之,接口是一组方法的集合,Go 语言中使用关键字 type interface 定义。

接口的语法结构如下:
type 接口名 interface{
    方法1()
    方法2()
    ...
    方法n()
}
接口有以下特性:
接口虽然是一种类型,但它并没有描述具体的值是什么,也不会告诉你它的基础类型是什么,以及数据是如何存储的。接口仅描述这个值能做什么,有什么方法。

比如对于一个叫作“可以写字的工具”的接口来说,凡是满足“写字”这个方法的事物都可实现这个接口,无论是钢笔、铅笔、记号笔还是笔记本电脑。

注意,接口的定义非常简单,但要注意,接口由使用者定义!这是 Go 语言与其他面向对象的语言不同的地方。

Go语言接口支持鸭子类型

在提到接口时,可能有读者听说过 Go语言中关于鸭子类型(duck typing)的设计。那么什么是鸭子类型,它与接口有什么关系呢?

鸭子类型是动态类型的一种风格,是一种对象推断策略,它关注的是“对象的行为”,而非对象类型本身。

Go 语言用接口来支持鸭子类型。Go 语言的鸭子类型既具有 PythonC++ 中鸭子类型的灵活性,又有 Java 中类型检查的安全性。在 Go 语言中,接口的实现是隐式的,判断类型 T 是否为实现接口 I 的依据,就是检查 T 是否实现了接口 I 声明的所有方法。

我们来看一个示例。首先定义两种结构体 Foodie 和 Child。不同人眼中的鸭子是不一样的,对于美食家来说,烤鸭就是他眼中的鸭子;而对于小朋友来说,数字 2 就是鸭子。没人在乎鸭子是什么颜色,长着圆脑袋还是方脑袋。
// 定义 美食家 struct
type Foodie struct{}
 
//定义 小朋友 struct
type Child struct {}
接着,请美食家和小朋友分别使用 WhatIsADuck 这个方法来表达自己眼中的鸭子长什么样。请注意,WhatIsADuck() 方法是需求者(在此为这两种结构体)自己的行为。示例代码如下:
func (c *Foodie) WhatIsADuck() string {
    return "美食家眼中的鸭子是香喷喷的烤鸭"
}
 
func (c *Child) WhatIsADuck()string {
    return "小朋友眼中的鸭子是门前大桥下的24678"
}
接下来,我们想让美食家和小朋友在同一个舞台说出自己的想法。此时就轮到接口上场了,它提供了一种连接二者的方式。示例代码如下:
//定义了一个介绍鸭子的接口,连接介绍什么是鸭子的事物
//它的行为有:什么是鸭子
type IntroduceAboutDuck interface {
    WhatIsADuck() string
}
接口 IntroduceAboutDuck 对结构体 Foodie 和 Child 做了耦合,让两者有了共同的话题,即表达“自己眼中的鸭子是什么样的”。值得一提的是,此耦合是一个松耦合,有没有这个接口都不会对两个组件造成任何的影响。

在二者之间有了关联或者协议(interface)后,他们就可以登上共同的舞台,说说自己眼中的鸭子是什么样的了。示例代码如下:
var who IntroduceAboutDuck //共同的舞台(接口)
 
who = new(Foodie) //组件1
fmt.Println(who.WhatIsADuck()) //输出:美食家眼中的鸭子是香喷喷的烤鸭
 
who = new(Child)//组件2
fmt.Println(who.WhatIsADuck()) //输出:小朋友眼中的鸭子是门前大桥下的24678
就这样,在接口(笔者认为也可以把接口理解为协议)的连接下,两个不同的结构体 Foodie 和 Child 使用同一个方法 WhatIsADuck 说出了自己眼中鸭子的模样。

从代码的角度来看,只要是实现了接口 type IntroduceAboutDuck interface 中 WhatIsADuck 方法的类型(可以是结构体,也可以是基本类型的别名),都可以认为遵循了协议(接口)IntroduceAboutDuck,它们就可以在这一件事上有相同的行为,只是表现的方式和结果不一样罢了。

注意,接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量。在前面的示例中,声明了接口 IntroduceAboutDuck 的变量 who,变量 who 又调用了接口中定义的方法 WhatIsADuck()。在实际调用时,会根据变量 who 包裹的类型去调用对应的 WhatIsADuck() 方法。

所谓鸭子类型模式,关注的仅仅是行为,只要实现了接口定义的方法(行为),那么就可以认为它是一只“鸭子”。在 Go 语言中,任何类型(可以是结构体,也可以是基本类型的别名)只要拥有一个接口需要的所有方法,那么它就实现了这个接口,不需要额外的显式声明。另外,不仅仅是结构体类型可以实现接口,拥有别名的类型同样也可以实现接口。

Go语言接口与协议

在 Go 语言中,把接口看作协议应该更容易理解。

这里通过一个类比来说明,我们每个人都有多种行为,如果将其中特定的行为组合在一起,并从这些具体行为中抽象出共同特性,那么这类共同特性就可以称为协议。如下图所示:


图 1 接口与协议

只要“他”拥有某个协议中定义的所有行为,就可以认为“他”属于这个群体。可以看到,人与人之间通过协议有了关联,协议仅仅是将具有特定行为的人做了一个耦合。

接口(协议)有约束的功能。例如,“PaaS平台”这个协议发布了两个条件(Kubernetes 和容器云运行时),只有同时满足这两个条件的事物,才可以被认为是一个 PaaS 平台。在下图中,华为容器云、腾讯容器云、阿里容器云、亚马逊容器云均满足约束条件。


图 2 接口的约束

为了让结构体的方法更规范,可以使用接口对方法进行约束。在面向接口编程时,这种方式可以起到规范的效果。

Go语言接口实现多态

Go 语言中的多态是通过接口实现的。我们来看看用接口实现多态的过程。

这里用驾驶各种车来举例。在下面的示例中,car 和 bike 两个结构体代表两种不同类型的车,基于它们各自的 drive() 方法可知,它们都可以做出 drive 的动作(行为)。
type car struct {}
 
type bike struct {
}
 
func (c car) drive() {
     fmt.Println("汽车用四个轮子")
}
 
func (b bike) drive() {
     fmt.Println("自行车用两个轮子")
}
 
func main() {
     carInstance := car{}
     carInstance.drive()
     bikeInstance := bike{}
     bikeInstance.drive()
}
从上述代码中可以看到,先在 main() 函数中对这两个对象进行了实例化(初始化),然后执行了两次“对象.方法”操作。如果增加更多的类型,那么每种类型都需要执行上述步骤。

为了改善这种情况,我们引入了接口。接口是一组方法的集合,它只定义不具体实现,实现的细节由与对象绑定的方法决定。我们加入如下代码:
type driver interface {
     drive()
}
因为这些类型都有相同的行为 drive,且其具体的实现又与绑定的对象有关,所以这里将之抽象出来,形成接口 driver。

我们加入 How2Drive() 函数,并修改 main() 函数的调用方式:
func How2Drive(what driver) {
     what.drive()
}
 
func main() {
     carInstance := car{}
     bikeInstance := bike{}
     How2Drive(carInstance)
     How2Drive(bikeInstance)
}
在 Go 语言中,想要实现一个接口,只需要实现这个接口所定义的所有方法即可。在上述代码中,结构体 car 和 bike 都拥有签名相同的方法 drive,可见这两个结构体都实现了接口 driver。在调用 How2Drive() 函数时,传入不同的实例(carInstance 或者 bikeInstance),执行同一个动作(drive),完成的是不同的具体操作,这也就表明实现了多态。

相关文章