Go
语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。著名的Duck Typing
非常好的解释了接口的特性即“这个东西叫起来像鸭子,走起来像鸭子,那可以认为它就是鸭子”。接口是典型的like-a
的判断关系,而且在这种语言中是隐式的实现接口。
接口作为结构体内嵌
接口作为函数或者方法的参数
接口作为返回值
接口作为其他接口的内嵌
使用接口
接口声明
类似于结构体的声明,接口的声明分为关键字,接口名。不太相同的是里面的元素是函数签名或者直接定义的函数类型名称。当然也可以像结构体一样进行匿名嵌套,实现接口的继承。其中如果是方法,方法声明等于方法名加上方法签名。
接口的命名一般以
er
结尾接口定义的方法不需要关键字
func
接口定义中只需要声明方法,不需要实现
实现接口
实现接口如上文所说的,只要其他类型实现了该接口的所有的方法,那么这就是实现了接口的实例,这就是经典的鸭子理论。
type Animal interface {
GetName()
}
// The function in the interface is realized through the following Cat and Dog
// structures, and it becomes an instance of the interface.
type Dog struct {
name string
}
func (d *Dog) GetName() {
fmt.Println(d.name)
}
type Cat struct {
name string
}
func (c *Cat) GetName() {
fmt.Println(c.name)
}
接口赋值
接口是一种类型,和普通的变量并无两样,所以也可以赋值和运算。如果把小接口赋值给大接口编译器会报错,如果把大接口给小接口那么赋值过后能够使用的方法将被收窄到和小接口的方法集一致。这一点和C++
中的类的指针多态有一点相像,可以把大接口理解为指向基类而且能调用函数的指针。
type Animal interface {
Move()
}
type Bird interface {
Move()
Fly()
}
func main() {
var a Animal
var b Bird
// At this time the method set has changed.
a = b
}
空接口
所有类型都实现了空接口。因为没有任何方法,空集的方法集使得所有类型都可以实现空接口。空接口可以实现泛型和反射,前者可以使函数接收任何类型的参数,后者可以将类型的转换赋值给空接口后才进行处理。空接口不是真的空,而是他的方法集是空的,iter != nil
。
接口的运算
接口有方法知道自身到底变成了何种数据类型,并且可以经过这种查询来进行下一步具体的针对性的操作。
类型断言
类型断言是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
具体类型断言
对于具体的类型的断言,如果断言成功则会返回一个该类型的值的拷贝,否则会抛出panic
。
a := i.(int)
// If the assertion is successful, former will have value and type of the
// latter. The variable will become an instance of implementing the interface.
接口类型断言
对于接口类型的断言会判断是否这个实例是否实现了该接口的类型会返回一个相同的接口对象,同样不匹配也会抛出异常。
a := i.(Animal)
// If the assertion is successful, former will be the interface correct.
防止异常
上面的两个断言方式都很有风险,那就是会抛出异常。这时候也可以使用一种新的方式来进行接口断言。如果不满足就会把标志的布尔值赋值为false
而不是直接异常导致程序崩溃。
if a, ok := i.(float64); ok {
// code...
}
接口查询
接口查询是Go
实现类函数重载的一种方法,当然也不一定非要用来实现这种编程方式。这样可以查询接口变量底层绑定的底层变量的具体类型,还可以查询这个接口变量是否实现了某一个其他的接口。接口的查询主要靠的是switch
语句和case
语句。
- 基础语法
接口查询的语法和类型断言的语法很相似,只不过后面的类型名或接口名改成了.(type)
。
var any interface{}
// Any type can be an instance of an empty interface.
switch v := any.(type) {
case nil:
// code...
case int:
// code...
case bool:
// code...
default:
// code...
}
当然对于接口查询还可以查询是否实现了某个接口,严格意义上说case
后面既可以跟具体的类型,也可以跟接口类型。
f, _ := os.OpenFile("text.txt", os.O_RDWR|os.CREATE, 0744)
var anyReader io.Reader = f
switch reader := anyReader.(type) {
case io.ReadWriter:
// code...
case *os.File:
// code...
default:
// code...
}
这两种方法都尽量设置一个default
的选项来避免没有操作执行,其中可以写一些提示信息来表明为什么没有匹配的类型。还有如果匹配成功,前面的值依旧是后面变量对应的值拷贝,如果是指针的话就是指针值的拷贝也就是地址的复制。
接口的意义
-
解耦:复杂系统进行垂直和水平的分割是常用的设计手段,在层和层之间使用接口进行抽象和解耦是一种好的策略,而且在
Go
中是隐式的实现接口使得代码更加的干净。 -
实现泛型:现阶段的
Go
并不支持泛型编程,那么空接口就可以实现这些类型的代表性的匹配,设置一个通用参数相当于Class T
。
深析接口
面向对象编程OOP
中三个基本特征分别是封装,继承,多态。在Go
语言中封装和继承是通过结构体来实现的,而多态则是通过接口来实现的。
底层结构体
非空接口的定义是这样的,初始化过程就是一个初始化接口的iface
结构体。
type iface struct {
tab *itab // pointer information for storing types and methods
data unsafe.Pointer // data pointer point to the data information
}
空接口则不太一样,它只关注这个接口绑定的实例的类型,使用eface
结构体。
type eface struct {
_type *_type // instance's type
data unsafe.Pointer // data pointer to instance
}
tab
:用来存放接口自身类型和绑定的实例类型和函数指针
data
:只想接口绑定的实例副本所以接口初始化也是值拷贝
核心基础
在接口的结构体中itab
是接口的核心,主要是如下结构体。
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
function [1]uintptr
}
inter
:是只想接口类型元信息的指针
_type
:指向接口存放的具体类型的指针
hash
:具体类型的Hash
值,为了支持接口断言
fun
:函数的指针,实际上这个数组的大小是可以变化的
其实还有更加核心的接口底层的结构,但是到这一层就已经可以知道接口类型是怎么被值填充,并且存储它对应的方法的。Go
语言中弱化了指针的显式运用,但是其实在底层中的运用非常多,很多时候都是用指针来指向内存空间,类型和绑定的方法是接口的基础。
调用过程
- 第一步
构建iface
的动态数据结构,是接口有实例指定的时候进行的。
- 第二步
通过函数指针间接的调用所绑定的实例的方法,这个就是接口方法的调用时进行。