Go语言中的接口类型会根据是否包含一组方法而分成两种不同的实现,分别为包含一组方法的iface结构体和不包含任何方法的eface结构体。我们将从这两个结构的底层数据结构说起,然后在interface编译时具体类型赋值给接口时是如果进行转换的。
iface和eface的底层数据结构在src/runtime/runtime2.go文件中
我们先来看eface的结构,相对于iface它的结构比较简单
type eface struct {
_type *_type // 空接口具体的实现类型
data unsafe.Pointer // 具体的值
}
data字段是eface和iface都有的,是一个内存指针,指向接口数据的存储地址,再看_type,它实际是在src/runtime/type.go中,
type _type struct {
size uintptr //类型大小
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 //hash值
tflag tflag //类型的flag和反射相关
align uint8 //内存对齐
fieldAlign uint8
kind uint8 //基础类型枚举值,有26个基础类型
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个形参对应对象的类型是否相等
gcdata *byte
str nameOff
ptrToThis typeOff
}
我们知道Go 语言是强类型语言,编译时对每个变量的类型信息做强校验,所以每个类型的元信息要用一个结构体描述。再者 Go 的反射也是基于类型的元信息实现的。_type 就是所有类型最原始的元信息。像类型名称,大小,对齐边界,是否为自定义类型等信息,是每个类型元数据都要记录的。所以被放到了runtime._type结构体中。
编辑切换为居中
eface数据结构
再看iface,与eface不同的是iface结构体中要同时存储方法信息,它的结构如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
tab 中存放的是类型、方法等信息,data 指针指向的 iface 绑定对象的原始数据
type itab struct {
inter *interfacetype // 接口自身定义的类型信息,用于定位到具体interface类型
_type *_type // 接口实际指向值的类型信息-实际对象类型
hash uint32 // itab.hash是从itab._type中拷贝来的,是类型的哈希值,用于快速判断类型是否相等时使用
_ [4]byte
//variable sized. fun[0]==0 means _type does not implement inter
fun [1]uintptr
// 动态数组,接口方法实现列表(方法集),即函数地址列表
}
itab的_type就是iface的动态类型,就是赋值给接口类型的那个变量的数据类型,跟eface指向的是同一个结构。
itab的inter是interface的类型元数据,它里面记录了这个接口类型的描述信息,接口要求的方法列表就记录在interfacetype.mhdr这里。
itb的fun当fun0为0时,说明_type并没有实现该接口,当有实现接口时,fun存放了第一个接口方法的地址,其他方法一次往下存放,这里就简单用空间换时间,其实方法都在_type字段中能找到,实际在这记录下,每次调用的时候就不用动态查找了。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
编辑切换为居中
iface数据结构
type Eater interface {
Eat(foodName string)
}
type Dog struct {
}
func (d Dog) Eat(food string) {
fmt.Println("dog eat", food)
}
//判断结构体Dog是否实现了Eater接口
//保证接口都被实现,否则在编译时就会报错(推荐使用)
var _ Eater = Dog{}
func main() {
var eat Eater = Dog{}
eat.Eat("meat")
}
//结果打印:dog eat meat
interface的底层数据结构我们已经知道了,但是为什么底层结构在runtime中,究竟在编译的时候是怎么转换的呢?
我们在分析Go底层的时候往往会通过汇编来看,对我而言对汇编不太清楚的,不过通过查阅资料了解了一些interface在编译期间怎么进行转换的,有一句话对Go编译描述的很巧妙。
几乎没有任何一个 Go 汇编底层问题不是用一条 go tool compile 不能解决的,如果不行的话,就用 go tool objdump,总能知道是怎么回事
go tool compile -S main.go // 反编译代码为汇编代码
go tool objdump // 可用于查看任意函数的机器码、汇编指令、偏移
Go程序在编译的时候会生成汇编,汇编器会将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都与一条机器指令相对应。
//上面的举例代码经过go tool compile -S 之后有这么一个runtime的调用函数
CALL runtime.convT2I(SB) // 具体类型赋值给接口类型是,这里调用的是convT2I()
// Type to non-empty-interface conversion.
func convT2I(tab *byte, elem *any) (ret any)
//具体类型赋值给空接口
// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
convT2I 函数的实现在src/runtime/iface.go,根据tab._type的类型的大小t.size 使用mallocgc申请一块内存空间,然后将elem指针的内容拷贝到申请的空间x,然后对iface的tab和data进行赋值,这样就完成了iface的创建。
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
convT2E 和 convT2I类似,同样在转换成eface时*_type是由编译器生成,当做入参调用convT2E
// 空接口转换函数convT2E实
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
// TODO: We allocate a zeroed object only to overwrite it with actual data.
// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}