前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go-接口interface底层实现

Go-接口interface底层实现

原创
作者头像
小许code
发布2023-02-27 09:32:00
5980
发布2023-02-27 09:32:00
举报
文章被收录于专栏:小许code

前言

Go语言中的接口类型会根据是否包含一组方法而分成两种不同的实现,分别为包含一组方法的iface结构体和不包含任何方法的eface结构体。我们将从这两个结构的底层数据结构说起,然后在interface编译时具体类型赋值给接口时是如果进行转换的。

iface和eface的底层数据结构在src/runtime/runtime2.go文件中

1:eface

我们先来看eface的结构,相对于iface它的结构比较简单

代码语言:javascript
复制
type eface struct {
 _type *_type     // 空接口具体的实现类型
 data  unsafe.Pointer // 具体的值
}

data字段是eface和iface都有的,是一个内存指针,指向接口数据的存储地址,再看_type,它实际是在src/runtime/type.go中,

代码语言:javascript
复制
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数据结构

2:iface

再看iface,与eface不同的是iface结构体中要同时存储方法信息,它的结构如下:

代码语言:javascript
复制
type iface struct {
 tab  *itab
 data unsafe.Pointer
}

tab 中存放的是类型、方法等信息,data 指针指向的 iface 绑定对象的原始数据

代码语言:javascript
复制
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字段中能找到,实际在这记录下,每次调用的时候就不用动态查找了。

代码语言:javascript
复制
type interfacetype struct {
 typ     _type
 pkgpath name
 mhdr    []imethod
}

编辑切换为居中

iface数据结构

3:举例代码

代码语言:javascript
复制
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

4:编译时如何转换

interface的底层数据结构我们已经知道了,但是为什么底层结构在runtime中,究竟在编译的时候是怎么转换的呢?

我们在分析Go底层的时候往往会通过汇编来看,对我而言对汇编不太清楚的,不过通过查阅资料了解了一些interface在编译期间怎么进行转换的,有一句话对Go编译描述的很巧妙。

几乎没有任何一个 Go 汇编底层问题不是用一条 go tool compile 不能解决的,如果不行的话,就用 go tool objdump,总能知道是怎么回事

代码语言:javascript
复制
go tool compile -S main.go  // 反编译代码为汇编代码

go tool objdump // 可用于查看任意函数的机器码、汇编指令、偏移

Go程序在编译的时候会生成汇编,汇编器会将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都与一条机器指令相对应。

代码语言:javascript
复制
//上面的举例代码经过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)

4.1:convT2I

convT2I 函数的实现在src/runtime/iface.go,根据tab._type的类型的大小t.size 使用mallocgc申请一块内存空间,然后将elem指针的内容拷贝到申请的空间x,然后对iface的tab和data进行赋值,这样就完成了iface的创建。

代码语言:javascript
复制
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
}

4.2:convT2E

convT2E 和 convT2I类似,同样在转换成eface时*_type是由编译器生成,当做入参调用convT2E

代码语言:javascript
复制
// 空接口转换函数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
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1:eface
  • 2:iface
  • 3:举例代码
  • 4:编译时如何转换
    • 4.1:convT2I
      • 4.2:convT2E
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档