[TOC]
接口
一 Go语言中的接口介绍
Go语言中的接口是什么?
* 1)是一种类型
* 2)是一组功能的集合,而不是一个功能
* 3)接口内只定义函数,但不涉及函数实现
* 4)这些功能是相关的,都是相关的功能,比如动物的接口里应该放动物相关的功能,但光合作用就不适宜放到IAnimal里面了 */
为何要用接口?
Go语言提倡面向接口编程
接口规定了必须要有哪些函数以及函数的名字是什么,然后让“子类“去实现接口的函数,以此来把多个子类统一/规范起来
这么做的意义在于归一化,什么叫归一化,就是只要是基于同一个接口实现的类,那么所有的这些类产生的对象在使用时,从用法上来说都一样。
// 归一化的好处在于:
1. 归一化让使用者无需关心对象的类是什么,只需要的知道这些对象都具备某些功能就可以了,这极大地降低了使用者的使用难度。
2. 归一化使得高层的外部使用者可以不加区分的处理所有接口兼容的对象集合
2.1:就好象linux的泛文件概念一样,所有东西都可以当文件处理,不必关心它是内存、磁盘、网络还是屏幕(当然,对底层设计者,当然也可以区分出“字符设备”和“块设备”,然后做出针对性的设计:细致到什么程度,视需求而定)。
2.2:再比如:我们有一个汽车接口,里面定义了汽车所有的功能,然后由本田汽车的类,奥迪汽车的类,大众汽车的类,他们都实现了汽车接口,这样就好办了,大家只需要学会了怎么开汽车,那么无论是本田,还是奥迪,还是大众我们都会开了,开的时候根本无需关心我开的是哪一类车,操作手法(函数调用)都一样
二 接口的定义
接口是一种类型,该类型内部包含多个未实现的函数/方法,定义语法如下
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
注意:
-
1、接口类型的命名应该见名知意,一般会在单词后加er,
例如:
- 有读操作的接口:Reader - 写操作的接口:Writer - 关闭操作的接口:Closer - 其他:Printer、Logger、Converter等等
还有一些不常用的方式(当后缀
er
不合适时),比如Recoverable
,此时接口名以able
结尾,或者以I
开头(像.NET
或Java
中那样)。 -
2、Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
-
3、接口名以及方法名的首字母均为大写,该方法才可以被其他包导入
例如:
package io type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error }
-
4、参数列表与返回值列表中的参数名字可以省略
例如
type Reader interface { Read([]byte) (int, error) }
三 接口的实现
Go中,子类不需要显式声明它实现了某个接口,只要“子类”(事实上,go语言中struct充当的就是类的概念)实现了接口中全部的函数那就是实现了接口,当然,Go的这种风格也可以被称为隐式实现,并且即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。
所有这些特性使得接口具有很大的灵活性。
// 注意:必须实现全部接口的内的函数,才算实现了接口,少一个都不行
// 1、定义接口
type animaler interface {
eat()
sleep()
}
// 2、定义接口提
type dog struct {
name string
class string
}
// 3、实现接口
// 3.1、实现接口内的方法eat
func (d dog) eat() {
fmt.Printf("一只名为【%v】的【%v】正在吃东西\n",d.name,d.class )
}
// 3.2、实现接口内的方法sleep
func (d dog) sleep() {
fmt.Printf("一只名为【%v】的【%v】正在睡觉\n",d.name,d.class )
}
// 3.3、结构体d当然可以实现接口之外的方法,没有影响
func (d dog) run() {
fmt.Printf("一只名为【%v】的【%v】正在奔跑\n",d.name,d.class )
}
func main() {
d:=dog{
name: "天蓬元帅",
class: "哈士奇",
}
d.eat()
d.sleep()
d.run()
}
Go语言中的接口是非侵入式的,即便是拿掉了接口,也不影响”子类“,所以说Go的接口也属于duck-type
四 声明接口类型的变量
不像大多数面向对象编程语言,在 Go 语言中可以声明接口类型的值,简称接口值,如下所示,a是一个多字(multiword)数据结构,它的值是 nil
。
var a animaler
// !!!注意!!!
指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
一个接口变量就可以存储(或“指向”,接口变量类似于指针,但接口与指针不完全是一回事)任何类型的具体值,只要这个值实现了该接口类型的所有方法,所以,但凡是实现了接口的变量都可以赋值给a,
a=d // 只有变量d完全实现了接口的所有方法,才算是实现了接口,此处的赋值才会成功
// !!!注意!!!
赋值成功后,此时接口类型变量a中所包含的方法列表中的指针会指向被实现的接口方法,即指向d的方法,于是我们可以通过“a.接口中规定的方法”来访问d中对应的具体实现功能,如下所示
我们可以不考虑具体的类型,而直接使用对象的方法,比如我们只需要记住d是一个动物就可以完全按照动物的方法去调用
a.eat()
a.sleep()
a.run() // 错误,d赋值给接口变量a之后,a只能调用接口规定的属性,不能调用d所有的属性,这还用说吗,我们比较调用的是a下的东西,a中之后eat和sleep,只是它们指向了d的具体实现而已,别犯糊涂,我们可以把任意实现了接口的变量赋值给a,那时,a中方法列表的指针就指向了新的实现,a.eat()和a.sleep()调用的就是新的实现
也就说我们只需要学习接口中的方法就可以了,用就用接口类型的变量即可,具体的类型不需要考虑,这,就是接口类型变量的好处
关于接口变量的赋值,正常的变量赋值都适用
var a animaler = d
或
a:=animaler(d)
或
a:=d
如果我们仅仅只是想验证一下,某个结构体是否实现了接口,可以这么做
// 1、如果下述代码可以赋值成功,则证明结构体实现了接口,因为此处我们只想验证,所以变量名无关紧要,推荐用_代表
var _ 接口类型 = &结构体{}
// 2、例如:
var _ animaler = &dog{}
在介绍结构体的方法时,我们提及,方法的接收者可以是值也可以是指针,当结构体在实现接口规定的方法时,
无论采用的是值接收者还指针接收者,都算是实现了接口,都可以将实例化好的结构体对象赋值给接口变量,但是
1、如果采用的是全部都是值接收者,那么既可以将实例化好的结构体对象赋值给接口变量,也可以将实例化好的结构体对象的指针赋值给接口变量
// 注意:必须实现全部接口的内的函数,才算实现了接口,少一个都不行
package main
import "fmt"
// 1、定义接口
type animaler interface {
eat()
sleep()
}
// 2、定义接口提
type dog struct {
name string
class string
}
// 3、实现接口
// 3.1、实现接口内的方法eat====>接收者为值类型
func (d dog) eat() {
fmt.Printf("一只名为【%v】的【%v】正在吃东西\n", d.name, d.class)
}
// 3.2、实现接口内的方法sleep====>接收者也为值类型
func (d dog) sleep() {
fmt.Printf("一只名为【%v】的【%v】正在睡觉\n", d.name, d.class)
}
func main() {
d := dog{
name: "天蓬元帅",
class: "哈士奇",
}
var x animaler
x=d // 既可以将实例化好的结构体对象赋值给接口变量
x.eat()
x.sleep()
x=&d // 也可以将实例化好的结构体对象的指针赋值给接口变量
x.eat() // go语法糖,相当于(*x).eat(),下同
x.sleep()
}
2、如果采用的包含指针接收者,那么只能将实例化好的结构体对象的指针赋值给接口变量
// 注意:必须实现全部接口的内的函数,才算实现了接口,少一个都不行
package main
import "fmt"
// 1、定义接口
type animaler interface {
eat()
sleep()
}
// 2、定义接口提
type dog struct {
name string
class string
}
// 3、实现接口
// 3.1、实现接口内的方法eat====>接收者为指针类型,根据一致性原则,下面的方法也应该写成指针接收者,当然,也可以不遵循该原则,但只要包含指针接收者,就只能将实例化好的结构体对象的指针赋值给接口变量了
func (d *dog) eat() {
fmt.Printf("一只名为【%v】的【%v】正在吃东西\n", d.name, d.class)
}
// 3.2、实现接口内的方法sleep====>接收者也为值类型
func (d *dog) sleep() {
fmt.Printf("一只名为【%v】的【%v】正在睡觉\n", d.name, d.class)
}
func main() {
d := dog{
name: "天蓬元帅",
class: "哈士奇",
}
var x animaler
//x=d // 不能将实例化好的结构体对象赋值给接口变量
//x.eat()
//x.sleep()
x=&d // 只能将实例化好的结构体对象的指针赋值给接口变量
x.eat() // go语法糖,相当于(*x).eat(),下同
x.sleep()
}
五 多个类型实现多个接口
可以多个类型/结构体实现同一个接口,也可以一个类型实现多个接口
package main
import "fmt"
// 1、动物接口
type animaler interface {
eat()
sleep()
}
// 2、猎手接口
type hunter interface {
hunting() // 捕猎
}
// 3、一个类型实现多个接口:dog既实现了animaler又实现了hunter接口
type dog struct {
name string
class string
}
func (d dog) eat() {
fmt.Printf("一只名为【%v】的【%v】正在吃东西\n",d.name,d.class )
}
func (d dog) sleep() {
fmt.Printf("一只名为【%v】的【%v】正在睡觉\n",d.name,d.class )
}
func (d dog) hunting() {
fmt.Printf("一只名为【%v】的【%v】正在捕猎\n",d.name,d.class )
}
// 4、多个类型实现同一个接口:cat与dog都实现了animaler接口
type cat struct {
name string
}
func (c cat) sleep() {
fmt.Printf("一只名为【%v】的小花猫正在睡觉\n",c.name)
}
func main() {
d:=dog{
name: "天蓬元帅",
class: "哈士奇",
}
d.eat()
d.sleep()
d.hunting()
// dog实现了两种接口animal和hunter,所以下述两行代码均通过
var _ animaler = &dog{}
var _ hunter = &dog{}
c:=cat{"egon"}
c.sleep()
}