Go接口的实现

Go接口的实现

Go的接口只需要简单的声明方法就能实现一个接口,使用者在定义新类型的时候也不需要显示声明要实现的接口.但是接口的底层是如何实现的呢?如何实现动态调用?接口的动态调用有多大的额外开销呢?一起来了解一下吧

接口的数据结构

接口必须初始化才有意义,没有初始化的接口变量的默认值是nil,没有任何意义.具体类型传递给接口的过程称为接口的实例化,在接口实例化的过程中,编译器通过特定的数据结构来描述这个过程

一、非空接口

iface

非空接口的底层数据结构是iface,位于src/runtime/runtime2.go(192行,go1.13 linux/amd64)

type iface struct {
  tab *itab // itab 存放类型集方法指针信息
  data unsafe.Pointer // 指向底层数据信息
}
  • itab: 用来存放接口自身类型和绑定的示例类型以及示例的函数指针
  • data: 数据指针.指向接口绑定的示例的副本,接口的初始化也是一种值拷贝. 此处data指向具体的实例数据,如果传递给接口的是值类型,则data指向的是示例的副本,如果传递给接口的是指针类型,则data指向指针的副本(遵循Go里面值传递规则)

itab

下面是itab的数据结构(src/runtime/runtime2.go,730行,go1.13 linux/amd64):

type itab struct {
  inter *interfacetype // 接口自身的静态类型
  _type *_type // _type 就是接口存放的具体类型的实例(动态类型)
  hash  uint32 // 从_type.hash复制,用于type switch. 具体类型的hash值
  _     [4]byte
  fun   [1]uintptr // 变量大小,fun[0] == 0 意味着 _type 没有实现 inter.也就是说没有实现该接口
}
  • inter: 指向接口类型原信息的指针(定义接口的静态类型)
  • _type: 指向接口存放的具体类型的元信息(动态类型的元信息),ifacedata就是指向这个具体类型的值._type是实例类型的信息,data是实例类型的值
  • hash: 是实例类型的Hash值(从_type字段复制来的).这里冗余存放是为了接口断言或者类型查询时快速访问
  • func: 函数指针数组指针.指向的数据可以理解为实现接口的实例类型的实现了接口的函数表.这里定义只有一个元素,实际上指针数据的大小是可以变化的,由编译器负责填充.运行时使用golang底层的数据进行访问,不受struct类型越界检查的约束

itab这个数据结构是非空接口实现动态调用的基础,itab的信息被编译器和连接器保存下来,存放在可执行程序的自读存储段(.rodata)中.由于itab存放在静态分配的存储空间中,所以它不受GC的限制,它的内存也不会被回收.

_type字段是Go中类型系统的基础,包含了所有类型的共同元信息,编译器和运行时可以根据元信息解析具体的类型、类型名存放位置、类型的Hash值等基本信息.Go语言的类型元信息最初由编译器负责构建,并以表的形式存放在编译后的对象文件中,在由连接器在连接时进行段合并、符号重定向(填充某些值). 这些元信息在接口的动态调用和反射中被运行时引用(下期在介绍类型系统的实现)

interfacetype

interfacetype用来描述接口的类型(src/runtime/type.go, 354行,go1.13 linux/amd64),也就是存储声明接口的方法、接口所属包等信息:

type interfacetype struct {
  type _type // 类型通用部分.类型系统_type
  pkgpath name // 接口所属包名字信息,name 存放的不仅是名称,还有描述信息
  mhdr []imethod // 接口声明的方法
}
type imethod struct {
  name nameOff // 方法名在编译后的 section 里面的偏移量
  itype typeOff // 方法类型在编译后的 section 里面的偏移量
}

总结: 对于interface的理解从以上分析来看的话大致是这样:

  • 首先接口定义阶段:接口的定义应该在最初编译的时候就生成了一个itab的数据结构,这个结构作为该interface最原始的结构保存着这种接口元信息.然后实例化接口的时候,将实例类型的相关信息填充到itab这个结构,并生成了一个iface数据结构,我们使用的时候起始就是对这个iface进行操做.
  • 接口的调用阶段分为两个部分:
    • 第一阶段是iface动态数据的构建.这一阶段在结构初始化的时候完成的
    • 第二阶段是通过函数指针间接调用接口绑定的实例方法

接口调用代价

因为接口调用是一个动态的调用过程,包括iface结构的建立过程.一旦实例化以后,这个接口和具体类型的itab结构就是可以复用的,但是初始化的过程是有一定性能消耗的;当接口的方法调用的时候,由于他是一个函数指针的间接调用,这对现代的计算机CPU不是很友好,会导致CPU缓存失效和分支预测失败,这部分也是有部分消耗的

二、非空接口

interface{}是没有任何方法集的接口,所以空接口内部不需要维护和动态内存分配相关的数据结构itab.空接口只需要关心存放的具体类型是什么、具体类型的值是什么.空接口的底层数据结构如下(197行,go1.13 linux/amd64):

type eface struct {
  _type *_type // 具体类型数据
  data Unsafe.Pointer
}

从这可以看出,空接口不是真的为空.它保留了具体实例的类型和值拷贝,即便存放的具体类型是空的,空接口也不是空的 空接口自身没有方法集,所以他真正的意义是用于实现多态.下面这些方式都是使用了空接口的(将空接口类型还原):

  • 通过接口类型断言
  • 通过接口类型查询
  • 通过反射

引用文档:

  • Go语言核心编程

最后修改于 2023-10-09