北川广海の梦

北川广海の梦

Golang类型系统、接口与类型断言

304
2022-12-05

普通结构体

type Man struct{
	name string
}

func  (m *Man) Speak(){
}

我们定义了一个简单的结构体,它有一个name字段,并且为它实现了一个Speak的方法。这个对象在运行时的元数据如下
image-1670232897905
runtime._type结构,描述了类型的大小,是否自定义,类型名称等信息,是每一种类型都具备的。
uncommontype则描述了自定义类型的一些信息, 如包所在路径,方法的偏移量等。

type定义类型

通过type关键字,可以将一些内置类型,转变为一个自定义的类型。
例如:

type myInt = int32
type myInt2 int32

这两者一个的区别:type myInt=32这种写法,是给int32取了一个别名,myInt类型的对象,其对应的类型元数据与int32类型对象的类型元数据,是完全相同的一个。
所以,当我们尝试给myInt定义方法的时候,会得到错误:

type myInt = int32
func (m1 myInt) ff() { //编译器提示:无法为内置类型int32定义新方法

}

而myInt2,则可以理解为基于int32,创建了一个全新的类型,这个全新的类型元数据,与int32的类型元数据是不同的,所以可以为这个类型定义新方法。

type myInt2 int32

func (m1 myInt2) ff() {//编译不会报错

}

所以,当我们仅需要一个别名,来帮助我们提高代码可读性时,使用type myInt=int32这样的方式。但是当我们用到了某一个内置类型,其数据结构完全符合我们的需求,但是需要额外扩展方法的时候,请使用type myInt2 int32这样的方式。

接口

空接口

在go中, 空接口类型能被任何类型赋值。这点和object很像,在.NET和Java中,由于所有对象都是基于object派生,所以符合“子赋基”。但是golang的面向对象很有差异,结构体之间并不能直接这样操作。

interface{} 空接口,是如何实现能够接收所有类型的呢?
image-1670235061772
空接口对象含有两个关键字段,一个是_type,指向了当前空接口对象所处的类型元数据,另一个data字段,是一个指针,存储了对象所在的地址。

var e interface{} // 此时接口的data和_type都为nil
e = "123456" //赋值完成后,data指向”123456“这个对象,_type指向string的类型元数据

image-1670235743686

当然假如赋值的是一个值类型:参考如下代码

var num int = 123
var a interface{} = num

num是一个值类型,在赋值时,如果逃逸分析的结果是num会被分配到堆上,那么a的data指向的对象也就是这个堆上的对象。
而假如num是在栈上的,那么在赋值给a的时候,会隐式的产生一个num的拷贝变量num1,然后取这个num1的地址,赋值给a的data。

非空接口

非空接口,从逻辑上讲:就是拥有一系列方法定义的空接口。
image-1670242877968
可以看到,iface对象,拥有一个data指针,这和空接口一样,指向了被分配的对象。而重点就在这个itab上,*interfacetype指针,指向了接口本身的元数据,其中包含了接口本身的包名、方法列表等。
_type字段,和空接口一样,指向了被赋值的对象类型元数据。hash拷贝了被赋值的对象类型元数据的hash值,用于快速的比较判断类型是否相等。fun,则存储具体类型中了实现了该接口的方法列表中的方法地址。

此时我们可以思考一下,如果接口类型和具体类型是确定的,那么就可以确定一个唯一的itab对象,并且itab其中的数据都是固定不变的。所以,itab是可以复用的。事实上,GO也确实是这么做的。所有的itab会被放进一个Map(并非内置的那个map)中,key是"接口类型"+”具体类型“,value就是对应的itab。

var reader io.Reader
reader = &bytes.Buffer{}
var reader2 io.Reader
reader2 = &bytes.Buffer{}

reader和reader2,由于其接口类型和具体类型都是io.Reader和bytes.Buffer,所以对应的itab是同一个。同时,这里有一个需要注意的地方,bytes.Buff中,实现Read方法的时候,是对*bytes.Buffer实现的,所以赋值给接口io.Reader的时候,必须赋值指针,否则编译器会报错。

类型断言

类型断言,是Go语言类型系统提供的一种运行时判断对象类型的方式。

var a interface{}= "123"
v,ok=a.(string)

类型断言支持:

  • 空接口类型,断言具体类型
  • 空接口类型,断言接口类型
  • 非空接口类型,断言具体类型
  • 非空接口类型,断言接口类型

空接口断言具体类型

这种类型断言最简单,空接口中含有一个*type指针,指向其具体类型,只要判断是否指向的是要断言的类型元数据就行了。

非空接口,断言具体类型

通过非空接口与具体类型,可以通过去上文提到的缓存itab的map中,找到对应的itab,然后判断这个非空接口中的itab指针指向的是否就是对应的itab就可以了。

空接口断言接口类型

先空接口的_type指针,查询到其具体类型的类型结构体,然后可以先判断一下这个具体类型和接口类型是否有itab缓存,如果有缓存,则判断其itab的fun[0]==0,如果为0,则说明并没有实现具体类型。如果没有缓存,就需要去检查具体类型的方法列表,和接口类型元数据中定义的方法列表是否符合要求了。判断完毕之后,任然会将接口与方法的组合缓存在itab起来,只是其fun[0]可能为0,这样每种类型组合的方法列表,只会匹配一次,提高了性能。

非空接口,断言接口类型

类似的,会从非空接口中,找到itab,然后通过itab中的 *_type找到具体类型的元数据,然后和目标类型进行itab缓存的查找,如果存在缓存,且fun[0]不为0,则断言成功,为0则断言失败。如果itab缓存不存在,则还是得对比方法列表,然后同样给缓存起来。
非空接口和空接口在接口类型的断言很相似,只是空接口无需再从itab去找具体类型元数据的指针。