Go 接口-契约介绍
目录
一、接口基本介绍
1.1 接口类型介绍
接口是一种抽象类型,它定义了一组方法的契约,它规定了需要实现的所有方法。是由 type 和 interface 关键字定义的一组方法集合,其中,方法集合唯一确定了这个接口类型所表示的接口。
一个接口类型通常由一组方法签名组成,这些方法定义了对象必须实现的操作。接口的方法签名包括方法的名称、输入参数、返回值等信息,但不包括方法的实际实现。例如:- type Writer interface {
- Write([]byte) (int, error)
- }
复制代码 上面的代码定义了一个名为 Writer 的接口,它有一个 Write 方法,该方法接受一个 []byte 类型的参数并返回两个值,一个整数和一个错误。任何类型只要实现了这个 Write 方法的签名,就可以被认为是 Writer 接口的实现。
总之,Go语言提倡面向接口编程。
1.2 为什么要使用接口
现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。- 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语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码。
1.3 面向接口编程
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语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定,但是通常使用接口类型可以使代码更清晰易读。
1.4 接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:- type 接口类型名 interface{
- 方法名1( 参数列表1 ) 返回值列表1
- 方法名2( 参数列表2 ) 返回值列表2
- …
- }
复制代码 其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
下面是一个典型的接口类型 MyInterface 的定义:- type MyInterface interface {
- M1(int) error
- M2(io.Writer, ...string)
- }
复制代码 通过这个定义,我们可以看到,接口类型 MyInterface 所表示的接口的方法集合,包含两个方法 M1 和 M2。之所以称 M1 和 M2 为“方法”,更多是从这个接口的实现者的角度考虑的。但从上面接口类型声明中各个“方法”的形式上来看,这更像是不带有 func 关键字的函数名 + 函数签名(参数列表 + 返回值列表)的组合。
在接口类型的方法集合中声明的方法,它的参数列表不需要写出形参名字,返回值列表也是如此。也就是说,方法的参数列表中形参名字与返回值列表中的具名返回值,都不作为区分两个方法的凭据。
比如下面的 MyInterface 接口类型的定义与上面的 MyInterface 接口类型定义都是等价的:- type MyInterface interface {
- M1(a int) error
- M2(w io.Writer, strs ...string)
- }
- type MyInterface interface {
- M1(n int) error
- M2(w io.Writer, args ...string)
- }
复制代码 不过,Go 语言要求接口类型声明中的方法必须是具名的,并且方法名字在这个接口类型的方法集合中是唯一的。前面我们在学习类型嵌入时就学到过:Go 1.14 版本以后,Go 接口类型允许嵌入的不同接口类型的方法集合存在交集,但前提是交集中的方法不仅名字要一样,它的方法签名部分也要保持一致,也就是参数列表与返回值列表也要相同,否则 Go 编译器照样会报错。
比如下面示例中 Interface3 嵌入了 Interface1 和 Interface2,但后两者交集中的 M1 方法的函数签名不同,导致了编译出错:- type Interface1 interface {
- M1()
- }
- type Interface2 interface {
- M1(string)
- M2()
- }
- type Interface3 interface{
- Interface1
- Interface2 // 编译器报错:duplicate method M1
- M3()
- }
复制代码 上面举的例子中的方法都是首字母大写的导出方法,所以在 Go 接口类型的方法集合中放入首字母小写的非导出方法也是合法的,并且我们在 Go 标准库中也找到了带有非导出方法的接口类型定义,比如 context 包中的 canceler 接口类型,它的代码如下:
[code]// $GOROOT/src/context.go// A canceler is a context type that can be canceled directly. The// implementations are *cancelCtx and *timerCtx.type canceler interface { cancel(removeFromParent bool, err error) Done() |