关于Golang类型的讲解2——interface接口
接上回,关于Golang类型的讲解1——内置类型和自定义类型 。
前言
上回,我们主要讲了golang中内置类型和自定义类型的定义、区别、以及本质的含义。明白了类型元数据在其中扮演的是一个记录各个类型的底层基础信息的角色,也了解了自定义类型相比内置类型在结构上的差异,也因此知道了为什么自定义类型可以关联function,而内置类型不能。接下来我们继续拓展开来,讲解下interface
一、什么是interface
interface,顾名思义——接口。也即是接口类型。而在我们的接口类型中还分为:空接口类型和非空接口类型。
二、接口类型的组成
1.空接口类型的组成
首先,空接口类型的数据结构组成由两部分组成:
_type *_type (指向接口的动态类型元数据) |
---|
data unsafe.pointer(指向接口的动态值) |
对应结构体代码:
runtine
type eface struct {
_type *_type
data unsafe.Pointer
}
在此,我们举个例子。定义一个空接口类型,注意此时对应的元数据结构eface的成员变量 _type和data都为nil:
var emptyInter interface{} // eface{_type:nil,data:nil}
如果我们将某个变量a赋值给该空接口变量emptyInter,那么其成员变量:_type即是a的元数据结构信息;data则指向变量a。
举例:
var e interface{} // 对应的eface结构体: eface{_type: nil, data:nil }
f,_:=os.Open(“eggs.txt”) // 在这里,对应的f是*os.File类型,且os.File类型是自定义类型元数据,对应的组成
// | _type | uncommontype |
e = f // eface(_type:(*os.File的类型元数据) , data:f)
/*
eface{
_type: *os.File的类型元数据[_type|uncommontype]
Data: f *os.file类型对应的变量f。
}
*/
2.非空接口类型
有很多小伙伴可能不知道非空接口类型是什么,我在这里举个例子你就知道了:
type NonEmptyInter interface {
TryDoSomething(doKind int)(result string)
TryingDone()
}
以上就是非空接口类型,就是我们一贯的进行程序设计时用到的一种类型,我一开始用的时候都将其和c++的虚函数划等号,虽然其本质可能是不一样的。
作为非空接口类型的元数据结构,其当然和空接口的元数据结构是不一样的:
// 非空接口对应的类型元数据
type iface struct{
tab *itab //存储 非空接口要求的方法列表,以及接口动态类型信息
data unsafe.pointer // 与空接口一样,指向接口的动态值,可以参考空接口的os.File的举例
}
首先我们通过定义可以看到空接口和非空接口一个比较直观的区别:非空接口是有方法列表进行约束的,只要某个A类型实现了非空接口定义的所有方法,那么该A类型是可以转为该非空接口类型使用的。
而由非空接口类型元数据结构我们可以看到其和空接口类型的区别:非空接口类型元数据中有个itab类型的成员变量。
// itab结构体组成
type itab struct{
inter *interfacetype //指向一个interfacetype内置类型扩充数据信息(类似于slice type一样),我们可以理解为 当前的非空接口元数据类型的信息(在这里表示的是tryingBase的内置类型元数据的扩充数据基本信息,详情可以看下面的interfacetype结构)
_type *_type //动态类型元数据 (可以理解为实现类型的类型的元数据)
hash uint32 //类型hash值(用于快速判断类型是否相等)
_ [4]byte
fun [1]uintptr //(实现tryingBase非空接口的方法的地址,注意是实现,这里的fun其实是从_type(实现类型)中拿取实现的方法地址,主要便于快速定位获取实现方法,而不需要每次去到_type中获取)
}
逐个成员变量进行解析的时候,我们先举个例子来做参照
type NonEmptyInter interface {
TryDoSomething(doKind int)(result string)
TryingDone()
}
type A int
func (a A) TryDoSomething(doKind int)(result string){ return ""}
func (a A) TryingDone(){}
var nei NonEmptyInter
nei = new(A)
上述代码可以看到,定义了一个非空接口,并带有两个方法;定义一个自定义类型A,并关联实现了两个方法;定义非空接口类型变量——nei;通过newA将nei赋值。
- inter *interfacetyp。
对应的在上述例子中,此时,interfacetype会记录非空接口类型NonEmptyInter的的基础信息数据。
interfacetype结构:
type interfacetype struct {
typ _type //记录当前非空接口的类型元数据的基本信息(记录NonEmptyInter非空接口的类型名称,类型大小,对齐边界这些)
pkgpath name //NonEmptyInter类型所在包路径
mhdr []imethod //非空接口NonEmptyInter的方法列表(记录NonEmptyInter的两个方法的列表)
}
(题外话: 我们先会想下上一节讲的一个类型:slicetype,这个专门作为数组类型的变量的元数据类型,因为其除了记录数组类型的整体的基础底层信息外,还要记录数组中元素的单位项的统一底层基本信息。同理,golang也专门为我们的interface也有个interfacetype的元数据结构体。)
- _type *_type
成员变量_type主要是实现非空接口类型的类型元数据:也就是说_type是记录实现一方的(类型A)的类型元数据信息。
- hash uint32
首先在上面范例代码中因为一段代码:
nei = new(A)
实现了自定义类型A和非空接口类型NonEmptyInter的关联,所以基于这次关联会有一个hash值,作为它俩关联的一种唯一标示。
- fun [1]uintptr
这里记录的是类型A的两个实现方法的地址,且这里的fun的值其实是从实现类型A即itab的成员变量 _type中拿到的实现方法地址,主要便于快速定位获取实验方法,而不用每次都深入到成员变量_type中获取。
所以最终变量nei的类型元数据结构为:
iface{
Tab: //上文刚才讲的 A和NonEmptyInter的itab信息
data //指向实际的A类型的变量值信息 这个是可变的,在这里即为变量nei。
}
对于非空接口类型还有一个比较重要的概念,可复用性。什么叫可复用性质,首先我们看iface的组成结构,tab和data,首先data是变量的具体信息,而tab是非空接口类型和实现类型两种类型的一种组合,只有组合的差异性,而并不存在可变性。
所以golang底层会维护一个map表,用来专门存储tab的信息,也就是不同的非空接口类型和不同的类型的组合。
所以上述代码A和NonEmptyInter的组合也会存入golang底层维护的map表中。
所以问题就来了:
-
什么时候往这个map里塞入组合的tab信息?
首先当进行编译(待定?)的时候,会将这些代码中有的类似的赋值、强转、类型断言等情况纳入进来进行分析并组合成tab结构体存入到map表中。 -
存到map中有什么用?
可复用性,也就在此体现。首先当我这次遇到nei = new(A)的语句并将tab的组合存入到map表中后,后续的一系列NonEmptyInterface和A两种类型的强转和类型断言以及赋值等,都可以直接复用map表中的tab信息,不需要重新初始化,且在进行类型断言和强转的时候不需要进行校验俩类型是否符合,可以直接拿来用。
总结
本节主要讲解了interface接口的相信信息,包括:
- interface的两种形式和其结构组成——空接口和非空接口;
- 对于非空接口来说,其核心在于itab结构体的组成,且itab主要存储的是非空接口类型和实现类型的组合关系、以及实现类型所实现的方法的地址偏移量。
- 非空接口itab类型是可复用的,用一个map进行维护;
- 该map是根据代码中的两个类型之间的关系、包括强转、类型断言、赋值等进行初始化的,并不是无脑将所有的类型都进行组合存入;
- 注意:该map只负责存储,包括如果出现两个不能进行转换的类型作为组合存入map的时候,该map也会存入进来,但是其itab类型中的fun变量值为空,(不能互相转换那肯定是实现方法不一致呗)所以在接下来如果还有这两种类型的变量进行互转的时候, 可以直接从map中拿到这俩变量的类型组合通过成员变量fun进行判断,是否是有效转换。
- golang底层维护的itab的map有多种用途:强转、类型断言、赋值、反射等。