Go语言之接口(Interface)
接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。
Go 语言中使用组合实现对象特性的描述。对象的内部使用结构体内嵌组合对象应该具有的特性,对外通过接口暴露能使用的特性。
Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现,而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。
请牢记接口(interface)是一种类型,一种抽象的类型。
Go语言中接口的作用:
- 可以作为函数和方法的参数或者返回值的使用,可以通过类型断言和switch方法
- 多态的使用,在程序设计中,抽象出某些对象共同拥有的方法,多种类型实现同一接口,通过接口变量指向具体对象操作这些方法(在面向对象语言中,接口的多种不同的实现方式即为多态)
为什么要使用接口?
我们以支付接口为例
下面例子表示没有定义接口的时候,呈现的支付方式,微信和和支付宝都具备支付的动作,如图:

上图中的代码执行结果会显示两个支付样式,一个是支付宝支付,一个是微信支付,既然他们都具备相同的支付动作,那么是否可以将支付的方法提炼出来,做成一个接口,然后根据传入的实现类不同,去调用不同的支付方式,来实现多态功能,修改后的内容如图:

从上图中可看出,定义了支付接口,两个支付方式分别实现了此接口,通过函数callpayment传递给接口,接口根据传递内容的不同来调用不用的支付方式,从而实现了多态(想象一下一码支付的原理)
一、声明接口:
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
1、接口声明格式:
每个接口类型由数个方法组成。接口的形式代码如下:
type 接口类型名 interface{
方法名1 ( 参数列表1 )返回值列表l
方法名2 ( 参数列表2 )返回值列表2
....
}
- 接口类型名: 使用type 将接口定义为自定义的类型名。Go 语言的接口在命名时,一般会在单词后面添加er ,如有写操作的接口叫Writer , 有字符串功能的接口叫Stringer , 有关闭功能的接口叫Closer 等
- 方法名: 当方法名首字母是大写时, 且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package )之外的代码访问。
- 参数列表、返回值列表: 参数列表和返回值列表中的参数变量名可以被忽略

2、开发中常见的接口及写法
Go 语言提供的很多包中都有接口,例如io 包中提供的Writer 接口:
type Writer interface {
Write( p []byte ) (n int , err error )
}
这个接口可以调用Write()方法写入一个字节数组([]byte) ,返回值告知写入字节数(n int )和可能发生的错误( err error )
类似的,还有将一个对象以字符串形式展现的接口, 只要实现了这个接口的类型, 在调用String()方法时, 都可以获得对象对应的字符串。在fmt 包中定义如下:
type Stringer interface {
String( ) string
}
Stringer 接口在Go 语言中的使用频率非常高,功能类似于Java 或者C #语言里的ToString 的操作
Go 语言的每个接口中的方法数量不会很多。Go 语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口
二、实现接口:
接口定义后, 需要实现接口, 调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用
1、条件一:接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
为了抽象数据写入的过程,定义DataWriter 接口来描述数据写入需要实现的方法,接口中的WriteData()方法表示将数据写入,写入方无须关心写入到哪里。实现接口的类型实现WriteData 方法时, 会具体编写将数据写入到什么结构中。这里使用file 结构体实现DataWriter 接口的WriteData 方法,方法内部只是打印一个日志,表示有数据写入,如图:

- 第7行:首先定义DataWriter 接口。这个接口只有一个方法,即WriteData(),输入一个interface{}类型的data , 返回一个error结构表示可能发生的错误。
- 第10行:定义文件结构,用于实现DataWriter
- 第12行:file 的WriteData()方法使用指针接收器。输入一个interface{}类型的data,返回error
- 第17行:实例化file赋值给f, f 的类型为*file
- 第18行:声明DataWriter 类型的writer 接口变量
- 第19行:将f 赋值给DataWriter 接口的writer , 虽然两个变量类型不一致。但是writer 是一个接口, 且f 己经完全实现了DataWriter()的所有方法,因此赋值是成功的
- 第20行:DataWriter 接口类型的writer 使用WriteData()方法写入一个字符串。
从上图中可以看出,实现接口类型中的方法、参数列表、返回值类型与接口中定义的是一致的WriteData(data interface[]) error
代码执行结果如下:

当类型无法实现接口时,编译器会报错,下面列出常见的几种接口无法实现的错误:
- 函数名不一致导致的报错
- 实现接口的方法签名不一致导致的报错(参数,返回值)
2、条件二:接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用
例如:在上面接口中,添加一个方法,如图:

代码执行结果如下:

报错的原因是因为CanWrite()方法没有被实现,需要在file中实现CanWrite()方法才能正常使用DataWriter()
Go 语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计(只要实现了接口中的所有方法,就实现了此接口)
3、接口类型变量:
接口类型变量能够存储所有实现了该接口的实例
例如:下面例子表示通过接口类型变量来访问实例,如图:

- 第5-7行:声明Sayer接口,方法为say()
- 第8-9行:定义Cat和Dog两个结构体
- 第11-16行:Cat和Dog分别实现了say()方法,就可以实现Sayer接口,接收者类型为值接受者
- 第19行:声明Sayer类型的变量s
- 第20-21行:实例化Cat和Dog结构体
- 第22行:将Cat实例直接赋值给变量s
- 第23行:变量直接调用say()方法
代码执行结果如下:

注意:结构体方法必须实现了接口后才可给接口变量赋值,例如上面的结构体Cat和Dog结构体都实现了Sayer接口,因此a := Cat{}和b := Dog{}才不会报错
4、值接收者和指针接收者实现接口的区别
4.1 下面例子表示通过值接收者来实现接口,如图:

执行结果如下:

从上图中可以看出,a的类型为值类型,b的类型为指针类型,使用值接收者实现接口之后,不管是Cat结构体还是结构体指针*Cat类型的变量都可以赋值给该接口变量,因为Go语言中有对指针类型变量求值的语法糖,Cat指针b内部会自动求值*b
4.2 下面例子通过指针接收者实现接口,将上图中的接收者改为*Cat,如图:

代码执行结果如下:

从上图可以看出,执行结果报错了,因为接收类型为指针类型,因此不能传递值类型cat,现在将值类型的赋值注释,如图:

运行结果如下:

由于接收者类型为指针类型,因此只能给接口变量s传递指针类型的变量,s只能存储*Cat类型的值
5、下面例子将接口作为方法的返回值,如图:

- 第6行:定义接口interOne中的方法abc(),方法无参数,返回值为接口interTwo
- 第9行:定义接口interTwo中的方法def(),方法无参数,无返回值
- 第11行:定义结构体strOne
- 第14行:定义结构体strTwo
- 第18行:结构体strOne实现了接口interOne,因为方法名和返回值列表与接口保持一致
- 第23行:结构体strTwo实现了接口interTwo,因为方法名和返回值列表与接口保持一致
- 第27行:声明接口变量
- 第28行: 实例化结构体strOne
- 第31行:将实例化后的结构体赋值给接口变量,因为实现了接口,因此可赋值给接口
- 第32行:调用接口方法abc(),实际返回类型为interTwo,interTwo对应的值为strTwo结构体
运行结果如下:

说明:第18行,实现接口interOne后,返回值的类型为interTwo,由于interTwo为接口类型,只有实现了此接口的结构体,才可以将值赋值给此接口,上图中strTwo结构体实现了接口interTwo,因此可将实例化结构体作为interTwo的值,也就是上图中的19-21行
三、理解类型与接口的关系
类型和接口之间有一对多和多对一的关系
1、一个类型可以实现多个接口:
一个类型可以同时实现多个接口, 而接口间彼此独立,不知道对方的实现
下面例子定义说话和移动两个接口,并定义cat结构体,用来实现这两个接口,如图:

代码运行结果如下:

2、多个类型可以实现相同的接口:
Go语言中不同的类型还可以实现同一接口
下面例子表示多个类型实现同一接口,狗和汽车都实现了可以移动的接口,如图:

执行结果如下:

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
下面例子定义motion接口,其中两个方法,定义两个结构体,一个嵌入到另一个中,如图:

运行结果如下:

3、接口嵌套:
接口与接口间可以通过嵌套创造出新的接口,如图:


4、空接口:
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口,空接口类型的变量可以存储任意类型的变量,如图:


从上图中可以看出,像空接口中赋值任意类型的变量,都可以正常打印出来
声明空接口类型的切片,并赋不同的值,如下:


5、空接口的应用
空接口作为函数的参数,使用空接口实现可以接收任意类型的函数参数,如图:

空接口作为map的值,使用空接口实现可以保存任意值的字典,如图:

6、从空接口取值:
保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误,如图:

编译运行将出现错误,如图:

从上图可以看出,编译器提示不能将x变量作为int类型赋值给b,因为x的类型为interface{}类型,并且提示我们使用type assertion,也就是类型断言
下面通过类型断言来重新修改代码,如图:

代码执行结果如下:

注意:空接口没有任何方法,因此空接口类型的值不可调用别的方法,会报错,可通过类型断言来调用
7、类型断言:
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
t := i.(T)
- i 代表接口变量(注意:只有接口变量才有断言的功能)
- T 代表断言i可能是的类型(将i转换为T的类型)
- t 代表转换后的变量
如果i 没有完全实现T 接口的方法,这个语句将会触发宕机。触发宕机不是很友好,因此上面的语句还有一种写法:t,ok := i.(T),该语法返回两个参数,第一个参数是i转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败


上图中的例子如果要断言多次就需要通过多个if来实现,更好的办法是通过switch语句来实现,如图:

注意:x.(type)中的type为固定写法,不能替换成其他字符

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗
在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口
有三个操作符可以使用ok这样的bool类型变量,如下:
v,ok := map[key] //map映射
v,ok := x.T() //类型断言
v,ok := <- ch //通道接收
下面也是使用类型断言的例子,定义接口tempinter,接口中定义方法hello(),但是不包含world()方法,然后定义结构体aboutname,此结构体实现了接口tempinter,结构体还有额外的方法world(),结构体的方法接收器为指针类型,因此只能将类型的指针传递给接口变量,然后调用world方法,如图:

运行结果报错,提示没有方法,如图:

修改上面的代码,增加类型断言,然后再次调用world方法,如图:

程序运行如下:

上图中的类型断言可以理解为从接口变量tp中取回来类型aboutname,然后就可以调用world方法
注意:如果一个类型声明了指针接收器方法,你就只能将那个类型的指针传递给接口变量
8、比较空接口保存的值
当空接口类型是基础类型、数组类型或结构体类型时,不论空接口的类型是否相同,都可以使用”==”比较空接口保存的值


如果空接口类型是切片类型或映射类型,则无法比较空接口保存的值,如图:

提示切片只能与nil进行比较,如图:



