定义
interface是一组method签名的组合,我们通过interface来定义对象的一组行为。interface 是一种类型,定义如下:
1 | type Person interface { |
它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。golang
接口定义不能包含变量,但是允许不带任何方法,这种类型的接口叫empty interface
。
如果一个类型实现了一个interface
中所有方法,我们就可以说该类型实现了该interface
,所以我们我们的所有类型都实现了empty interface
,因为任何一种类型至少实现了0个方法。
实现接口
这里先拿java
语言来举例,在java
中,我们要实现一个interface
需要这样声明:
1 | public class MyWriter implments io.Writer{} |
这就意味着对于接口的实现都需要显示声明,在代码编写方面有依赖限制,同时需要处理包的依赖,而在Go
语言中实现接口就是隐式的,举例说明:
1 | type error interface { |
上面的代码,并没有error
接口的影子,我们只需要实现Error() string
方法就实现了error
接口。在Go
中,实现接口的所有方法就隐式地实现了接口。我们使用上述 RPCError
结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查。
Go
语言的这种写法很方便,不用引入包依赖。但是interface
底层实现的时候会动态检测也会引入一些问题:
- 性能下降。使用interface作为函数参数,runtime 的时候会动态的确定行为。使用具体类型则会在编译期就确定类型。
- 不能清楚的看出struct实现了哪些接口,需要借助ide或其它工具。
两种接口
Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:
- 使用
runtime.iface
表示包含方法的接口。 - 使用
runtime.eface
表示不包含任何方法的interface{}
类型,第二种在我们日常开发中经常使用到,所以在实现时使用了特殊的类型。
从编译角度来看,golang并不支持泛型编程。但还是可以用interface{}
来替换参数,而实现泛型。
interface内部结构
runtime.eface
结构体在Go
语言中的定义是这样的:
1 | type eface struct { // 16 字节 |
这里只包含指向底层数据和类型的两个指针,从这个type
我们也可以推断出Go语言的任意类型都可以转换成interface
。
另一个用于表示接口的结构体是 runtime.iface
,这个结构体中有指向原始数据的指针 data
,不过更重要的是 runtime.itab
类型的 tab
字段。
1 | type iface struct { // 16 字节 |
下面我们一起看看interface
中这两个类型:
runtime_type
runtime_type
是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。
1 | type _type struct { |
这里我只对几个比较重要的字段进行讲解:
size
字段存储了类型占用的内存空间,为内存空间的分配提供信息;hash
字段能够帮助我们快速确定类型是否相等;equal
字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从typeAlg
结构体中迁移过来的);
runtime_itab
runtime.itab
结构体是接口类型的核心组成部分,每一个 runtime.itab
都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter
和 _type
两个字段表示:
1 | type itab struct { // 32 字节 |
inter
和_type
是用于表示类型的字段,hash
是对_type.hash
的拷贝,当我们想将 interface
类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type
是否一致,fun
是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun
数组中保存的元素数量是不确定的;
内部结构就做一个简单介绍吧,有兴趣的同学可以自行深入学习。
空的interface(runtime.eface
)
前文已经介绍了什么是空的interface
,下面我们来看一看空的interface
如何使用。定义函数入参如下:
1 | func doSomething(v interface{}){} |
这个函数的入参是interface
类型,要注意的是,interface
类型不是任意类型,他与C语言中的void *
不同,如果我们将类型转换成了 interface{}
类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}
,之所以函数可以接受任何类型是在 go 执行时传递到函数的任何类型都被自动转换成 interface{}
。
那么我们可以才来一个猜想,既然空的 interface 可以接受任何类型的参数,那么一个 interface{}
类型的 slice 是不是就可以接受任何类型的 slice ?下面我们就来尝试一下:
1 | import ( |
这里我也是很疑惑,为什么Go
没有帮助我们自动把slice
转换成interface
类型的slice
,之前做项目就想这么用,结果失败了。后来我终于找到了答案,有兴趣的可以看看原文,这里简单总结一下:interface
会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的N*2
,但是 []T 的长度是N*sizeof(T)
,两种 slice 实际存储值的大小是有区别的。
既然这种方法行不通,那可以怎样解决呢?我们可以直接使用元素类型是interface的切片。
1 | var dataSlice []int = foo() |
问题
上面介绍了interface
的基本使用方法及可能会遇到的一些问题,下面出三个题,看看你们真的掌握了吗?
问题一
下面代码,哪一行存在编译错误?(多选)
1 | type Student struct {} |
答案:B、D;解析:我们上文提到过,interface
是所有go
类型的父类,所以Get
方法只能接口*interface{}
类型的参数,其他任何类型都不可以。
问题二
这段代码的运行结果是什么?
1 | func PrintInterface(val interface{}) { |
答案:this is non-empty interface
。解析:这里的interface{}
是空接口类型,他的结构如下:
1 | type eface struct { // 16 字节 |
所以在调用函数PrintInterface
时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*string
类型会转换成interface
类型,发生值拷贝,所以eface struct{}
是不为nil
,不过data
指针指向的poniter
为nil
。
问题三
这段代码的运行结果是什么?
1 | type Animal interface { |
答案:this is non-empty interface
. 解析:这里的interface
是非空接口iface
,他的结构如下:
1 | type iface struct { // 16 字节 |
d
是一个指向nil的空指针,但是最后return d
会触发匿名变量 Animal = p
值拷贝动作,所以最后NewAnimal()
返回给上层的是一个Animal interface{}
类型,也就是一个iface struct{}
类型。