GoLang之interface底层系列一(空接口、非空接口)

注:本文基于Windos系统上Go SDK v1.18进行讲解

1.空接口

空接口类型可以接收任意类型的数据,它只要记录这个数据在哪儿,是什么类型的就足够了。空接口变量数据结构如下,其中_type指向接口的动态类型元数据,data就指向接口的动态值。

//位于runtime/runtime2.go
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

2.空接口赋值前后

就像下面这个例子,为e赋值以前,其中存储的_type和data都为nil。

var e interface{}


如果我们把*os.File类型的变量f赋给e。那么变量e的结构如下图所示。
因为f本身就是个指针,所以e这里的data就等于f,动态类型就是*os.File。值得强调的是类型元数据这里是可以找到类型关联的方法元数据列表的,这一点对于理解“类型断言”至关重要。

f, _ := os.Open("eggo.txt")//返回*File
e = f

File结构体下面有很多方法

//os/types.go
type File struct {
	*file // os specific
}

3.非空接口

非空接口就是有方法列表的接口类型,一个变量要想赋值给一个非空接口类型,其类型必须要实现该接口要求的所有方法才行。

与空接口类型一样,这个data字段指向接口的动态值,iface.data记录的是接口的动态值,所以接口要求的方法列表以及与data对应的动态类型信息一定存在itab里面。

//位于runtime/runtime2.go
type iface struct {
    tab   *itab
    data  unsafe.Pointer
}

1.inter:接口的动态类型,也就是被赋给接口类型的那个变量的类型元数据(指向interface的类型元数据,接口要求的方法列表(mhdr)就记录在这里)
2._type: 指向接口的动态类型元数据
3.hash:是从itab._type中拷贝来的,是类型的哈希值,用于快速判断类型是否相等时使用。
4.fun:记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法,以便通过接口快速定位到方法,而无需再去类型元数据那里查找。如果itab._type对应的类型没有实现这个接口,则itab.fun[0]=0,这在类型断言时会用到。

//位于runtime/runtime2.go
type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
//go\src\runtime\type.go
//源码如下:
type interfacetype struct {
	typ     _type
	pkgpath name
	mhdr    []imethod
}
//runtime/type.go
type nameOff int32
type typeOff int32
type imethod struct {
	name nameOff
	ityp typeOff
}


4.非空接口赋值前后

如果我们声明一个io.ReadWriter类型的变量rw。被赋值以前,rw的data为nil,tab也为nil。

var rw io.ReadWriter


// io/io.go
type ReadWriter interface {
	Reader
	Writer
}
type Reader interface {
	Read(p []byte) (n int, err error)
}
type Writer interface {
	Write(p []byte) (n int, err error)
}

下面我们把一个*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。

var w io.Writer = f

此时w的动态值和动态类型与rw相同,只是二者的接口类型元数据不同,要求的方法列表也不同罢了。

5.itab缓存

关于itab我们还要额外关注一点,既然一个非空接口类型和一个动态类型就可以确定一个itab的内容,那这个itab结构体自然是可以被接口类型与动态类型均相同的接口变量复用的。

实际上Go语言会把用到的itab结构体缓存起来,并且以<接口类型, 动态类型>组合为key,以*itab为value,构造一个哈希表,用于存储与查询itab信息。

这个哈希表与map底层的哈希表不同,其结构设计更为简便。

//runtime/iface.go
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)的类型哈希值;
key的哈希值是这样计算的,用接口类型的类型哈希值,与动态类型的类型哈希值,进行异或运算(同值取0,异值取1),如果已经有对应的itab指针,就直接拿来使用

//runtime/iface.go
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
	// compiler has provided some good hash codes for us.
	return uintptr(inter.typ.hash ^ typ.hash)
}

如果能查询到对应的itab指针,就直接拿来使用。若没有就要再创建,然后添加到itabTable中。

了解了空接口和非空接口的数据结构,明确了接口动态值与动态类型在赋值前与赋值后的变化,接下来就可以看看“类型断言”是怎么回事儿了。