类型元数据
runtime._type 类型元数据
类型名称、类型大小、对齐边界、是否自定义等,是每个类型元数据都要记录的信息,所以被放到了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
信息
总结
类型断言的关键是明确接口的动态类型,以及对应的类型实现了哪些方法,而明确这些的关键还是类型元数据