类型名称、类型大小、对齐边界、是否自定义等,是每个类型元数据都要记录的信息,所以被放到了runtime._type结构体中,作为每个类型元素的Heade
在_type之后存储的是各种类型额外需要描述的信息,例如slice的类型元数据在_type结构体后面,记录着一个*_type指向其存储的元素的类型元数据,如果是string类型的slice,这个指针就指向string类型的元数据。
如果是自定义类型,后面还会有一个uncommontype结构体
| uncommontype | 意义 |
|---|---|
| pkgpath | 记录类型所在的包路径 |
| mcount | 记录该类型关联到多少个方法 |
| moff | 记录的是这些方法元数据组成的数组,相对于这个uncommontype结构体偏移了多少字节 |
例如我们基于[]string定义一个新类型myslice,他就是一个自定义类型,可以给它定义两个方法Len和Cap。
myslice类型元数据,首先是[]string的类型描述信息,然后在后面加上uncommontype结构体。
注:通过uncommontype这里记录的moff信息,我们就可以找到给myslice定义的方法元数据在哪儿了
如果uncommontype的地址为addrA,加上moff字节的偏移,就是myslice关联的方法元数据数组
利用类型元数据来解释下面两种写法的区别MyType1这种类型叫做给类型int32取别名,实际上MyType1和int32会关联到同一个类型元数据,属于同一种类型,rune和int32就是这样的关系。
MyType2这种写法属于基于已有类型创建新类型,MyType2会自立门户,拥有自己的类型元数据,即使MyType2相对于int32来说没有做任何改变,他们两个对应的类型元数据也已经不同了
每种类型都有唯一对应的类型元数据,而类型定义的方法能通过类型元数据找到
接口的数据结构 interface{} 空接口类型空接口类型可以接收任意类型的数据,它只要记录这个数据在哪,是什么类型
- _type 指向接口的动态类型元数据
- data 就指向接口的动态值
一个空接口类型变量,再被赋值以前_type和data都为nil
非空接口非空接口就是有方法列表的接口类型
一个变量要想赋值给一个非空接口类型,必须要实现该接口要求的所有方法才行
-
tab 接口要求的方法列表,以及接口动态类型信息,存在itab结构体中
-
data 就指向接口的动态值
-
inter 指向interface的类型元数据
- mhdr 接口要求的方法列表
-
_type 指向接口的动态类型元数据
-
hash 是从动态类型元数据中拷贝来的类型哈希值,用于快速判断类型是否相等时使用
-
fun 记录的是这个动态类型实现的那些接口要求的方法地址
此时这个itab结构体中的接口类型inter就是io.ReadWriter
动态类型_type为*os.File
注意:itab这里的fun,会从动态类型元数据中拷贝接口要求的那些方法的地址,以便通过rw快速定位到方法,而无需再去类型元数据哪里查找
itab结构体复用注:关于itab,一旦接口类型确定了,动态类型也确定了,那么itab的内容就不会改变了,所以itab结构体是可复用的。
Go语言会把用到的itab结构体缓存起来,并且以接口和动态类型的组合为key<接口类型,动态类型>,以itab结构体指针为value,构造一个hash表,用于存储与查询itab缓存信息
这里的hash表和map底层的哈希表不同,是一种更为简便的设计,需要一个itab时,会首先去这里查找。
key的哈希值是用类型接口的类型哈希值与动态类型的类型哈希值进行异或运算,如果有对应的itab指针,就直接拿来使用,若itab缓存中没有,就要创建一个itab结构体,然后添加到这个哈希表中
类型断言| 类型 | —— |
|---|---|
| 抽象类型 | 空接口,非空接口 |
| 具体类型 | int、string、slice、map、struct… |
类型断言作用在接口值之上.( )
空接口.( ) 非空接口.( )
断言的目标类型可以是具体类型或非空接口类型
.(具体类型) .(非空接口类型)
就组成四种类型断言
空接口.(具体类型)e.(*os.File)是要判断e的动态类型是否为*os.File,这只需要确定这个_type是否指向*os.File的类型元数据就好
注:Go语言里每种类型的元数据都是全局唯一的
e的动态值就是f,动态类型就是*os.File,如果成功r被复制为e的动态值,如果不成功就会被赋值为*os.File的类型零值nil
非空接口.(具体类型)rw.(*os.File)是要判断rw的动态类型是否为*os.File,就是经过一次判断,判断&itab是否指向这个itab结构体
空接口.(非空接口)注: 程序中用到的itab结构体都会缓存起来,可以通过接口类型和动态类型结合起来的key,查找到对应的itab指针
e.(io.ReadWriter)是要判断e的动态类型是否实现了io.ReadWriter接口。e动态值就是f,动态类型就是*os.File。
虽然*os.File类型元数据后面可以找到类型关联的方法元数据数组,也不必每次都去检查io.ReadWriter类型元数这里是否有对应接口要求的所有方法。因为有itab缓存,可以先去itab缓存中查找一下,如果没有io.ReadWriter和*os.File对应的itab结构体,再去检查*os.File的方法列表。
非空接口.(非空接口)注意:就算能够从缓存中查找到对应的itab,也要判断itab.fun[0]是否等于0,这是因为断言失败的类型组合其对应的itab结构体也会被缓存起来,只是会把itab.fun[0]置为0,用以标识这里的动态类型并没有实现对应的接口。
这样以后再遇到同种类型断言时就不用再去检查方法列表了,可以直接断言失败
w.(io.ReadWriter)是要判断w存储的动态类型是否实现了io.ReadWriter接口,w时io.Writer类型,接口要求一个Writer方法,io.ReadWriter要求实现Read和Writer两个方法。要确定*os.File是否实现了io.ReadWriter接口,同样会先去itab缓存里查找这个组合对应的itab指针,若存在,且itab.fun[0]不等于0,则断言成功,如不存在,再去检查*os.File的方法列表,并缓存itab信息
总结类型断言的关键是明确接口的动态类型,以及对应的类型实现了哪些方法,而明确这些的关键还是类型元数据



