“ 被动态值、动态类型整的晕头转向?还不快来认识下接口~”
空接口interface{}
空接口类型可以接收任意类型的数据,它只要记录这个数据在哪儿,是什么类型的就足够了。空接口变量数据结构如下,其中_type指向接口的动态类型元数据,data就指向接口的动态值。
type eface struct {
_type *_type
data unsafe.Pointer
}
就像下面这个例子,为e赋值以前,其中存储的_type和data都为nil。
图:e赋值前
f, _ := os.Open('eggo.txt')
e = f
如果我们把*os.File类型的变量f赋给e。那么变量e的结构如下图所示。因为f本身就是个指针,所以e这里的data就等于f,动态类型就是*os.File。值得强调的是类型元数据这里是可以找到类型关联的方法元数据列表的,这一点对于理解“类型断言”至关重要。
非空接口
非空接口就是有方法列表的接口类型,一个变量要想赋值给一个非空接口类型,其类型必须要实现该接口要求的所有方法才行。
type iface struct {
tab *itab
data unsafe.Pointer
}
iface.data记录的是接口的动态值,所以接口要求的方法列表以及与data对应的动态类型信息一定存在itab里面。
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
- itab.inter是interface的类型元数据,它里面记录了这个接口类型的描述信息,接口要求的方法列表就记录在interfacetype.mhdr这里。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
- itab._type就是接口的动态类型,也就是被赋给接口类型的那个变量的类型元数据。
- itab.hash是从itab._type中拷贝来的,是类型的哈希值,用于快速判断类型是否相等时使用。
- itab.fun记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法。如果itab._type对应的类型没有实现这个接口,则itab.fun[0]=0,这在类型断言时会用到。
非空接口赋值前后
如果我们声明一个io.ReadWriter类型的变量rw。被赋值以前,rw的data为nil,tab也为nil。
下面我们把一个*os.File类型的变量f,赋值给rw。f, _ := os.Open('eggo.txt')
rw = f
此时rw的动态值就是f,动态类型就是*os.File。而itab.fun这个数组里记录的是*os.File实现的Read、Write方法的地址。下面我们再声明一个io.Writer类型的变量w,并把f赋值给w。
此时w的动态值和动态类型与rw相同,只是二者的接口类型元数据不同,要求的方法列表也不同罢了。
itab缓存
关于itab我们还要额外关注一点,既然一个非空接口类型和一个动态类型就可以确定一个itab的内容,那这个itab结构体自然是可以被接口类型与动态类型均相同的接口变量复用的。
实际上Go语言会把用到的itab结构体缓存起来,并且以<接口类型, 动态类型>组合为key,以*itab为value,构造一个哈希表,用于存储与查询itab信息。
这个哈希表与map底层的哈希表不同,其结构设计更为简便。
type itabTableType struct {
size uintptr // length of entries array. Always a power of 2.
count uintptr // current number of filled entries.
entries [itabInitSize]*itab // really [size] large
}
需要一个itab时,会首先去itabTable里查找,计算哈希值时会用到接口类型(itab.inter)和动态类型(itab._type)的类型哈希值:
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
return uintptr(inter.typ.hash ^ typ.hash)
}
如果能查询到对应的itab指针,就直接拿来使用。若没有就要再创建,然后添加到itabTable中。
了解了空接口和非空接口的数据结构,明确了接口动态值与动态类型在赋值前与赋值后的变化,接下来就可以看看“类型断言”是怎么回事儿了。