跳转到正文
zeno's blog
返回

Go 基础:interface 的底层实现-eface 与 iface

Table of contents

Open Table of contents

接口

type Reader interface 底层通过2个结构体来实现,定义在源码的src/runtime/runtime2.go

type eface struct {
	_type *_type
	data unsafe.Pointer
}

type iface struct {
	tab *itab
	data unsafe.Pointer
}

type itab struct {
	inter *interfacetype // 接口本身的类型信息
	_type *_type // 实际对象的具体类型信息
	hash uint32
	_ [4]byte
	fun [1]uintptr // 函数指针数组:实际类型实现接口方法的内存地址
}
// 现代 Go 版本中的定义 (internal/abi/type.go)
type InterfaceType struct {
    Type      // 1. 头部依然是直接嵌入基础的类型元数据 (即 _type)
    PkgPath   Name      // 2. 接口所在的包路径
    Methods   []Imethod // 3. ⭐️ 核心:该接口要求实现的所有方法列表
}
type _type struct {
    size       uintptr // 类型占用的内存大小(字节数)
    ptrdata    uintptr // 包含所有指针的内存前缀大小(用于垃圾回收)
    hash       uint32  // 类型的 Hash 值(用于快速比较,如类型断言或作为 map 的 key)
    tflag      tflag   // 额外的类型标记(标志位,比如是否包含未导出字段)
    align      uint8   // 变量在内存中的对齐方式
    fieldAlign uint8   // 作为结构体字段时的对齐方式
    kind       uint8   // 类型的底层枚举种类(如 bool、int、slice、struct 等)

    // 用于比较两个该类型值是否相等的函数指针
    equal func(unsafe.Pointer, unsafe.Pointer) bool

    gcdata    *byte    // 垃圾回收的数据(记录该类型中哪些偏移量包含指针)
    str       nameOff  // 类型的名字(字符串在可执行文件中的偏移量)
    ptrToThis typeOff  // 指向这个类型的指针的类型(比如这是 int,那这就是 *int 的类型偏移量)
}

// `_type` 只是基础的通用信息。对于不同的具体类型,Go 在底层玩了一个“结构体嵌套”的魔法。
// 如果你声明了一个切片 `slice`,底层的真实元数据结构是 `slicetype`:

type slicetype struct {
    typ  _type // 基础的 _type 结构直接嵌在头部
    elem *_type // 切片内部元素的类型(比如 []int 这里的 elem 就是 int 的 _type)
}

// 同理,如果你声明了一个结构体 `struct`,底层就是 `structtype`,里面除了头部的 `_type`,还会包含一个记录结构体各个字段名称和偏移量的数组。

在 Go 1.20 之前的版本中,它叫 structtype,直接定义在 runtime 包里。但在 Go 1.20 及之后的版本中,Go 官方为了统一 runtimereflect 包底层的类型系统,将它重构到了 internal/abi 包中,并重命名为 StructType

// 现代 Go 版本中的定义 (internal/abi/StructType)
type StructType struct {
    Type                  // 1. 头部直接嵌入基础的类型元数据 (即我们之前说的 _type)
    PkgPath Name          // 2. 结构体所在的包路径
    Fields  []StructField // 3. 结构体内部所有字段的详细信息
}

type StructField struct {
    Name   Name    // 字段的名称 (比如 "Age", "Username")
    Typ    *Type   // 字段的具体类型指针 (指向 int, string 等的元数据)
    Offset uintptr // ⭐️ 极其关键:该字段在结构体内存中的【字节偏移量】
}

这里的 Name 是一个特殊的结构体(abi.Name),它本质上是一个指向可执行文件只读数据段(RODATA)的指针。Go 编译器在编译时,会把字段名和 Tag 连在一起,打包成一段紧凑的字节流。

这段字节流在内存中的排布规则大致如下:

  1. Flag(标志位):1 个字节。其中有一位专门用来标记**“这个字段有没有 Tag”**。
  2. 名字的长度:动态变长字节(Varint)。
  3. 名字本身:例如 "Age" 的 ASCII 码。
  4. Tag 的长度:(只有当 Flag 标记有 Tag 时才存在)。
  5. Tag 本身:例如 `json:"age" valid:"required"` 的 ASCII 码。

当你调用 reflect.TypeOf(obj).Field(0) 时,reflect 包底层的逻辑就是:

  1. 找到底层的 abi.StructField
  2. 读取它的 Name 指针指向的字节流。
  3. 检查 Flag 看看有没有 Tag。
  4. 如果有,就越过字段名,把后面的 Tag 字符串切片提取出来,赋值给这个面向开发者的 Tag 字段。

只有当一个接口的 Type 和 Data 都是 nil 时,这个接口才等于 nil

例子

这段代码是一个非常经典的 Go 语言面向接口编程的例子。

因为 Singer 是一个包含方法的接口,所以变量 s 在底层的真实结构是我们之前讨论过的 iface

当你执行 var s Singer = &Person{} 时,Go 语言在底层为你构建了一个 16 字节(64位系统下)的 iface 结构体。让我们一层一层剥开,看看这 16 个字节里到底装了什么。


第一层:s 的本体 (iface)

在底层,变量 s 其实就是这样一个包含两个指针的结构体:

Go

type iface struct {
    tab  *itab           // 8 字节:指向接口表(记录了类型信息和方法地址)
    data unsafe.Pointer  // 8 字节:指向实际的数据
}

在这段代码的具体场景中,这两个指针的指向如下:

1. data 指针(指向实际数据)

因为你赋值的是 &Person{}(一个指针),所以 s.data 这个万能指针,直接存储了 Person{} 结构体在堆(或栈)上的内存地址

换句话说,data 直接指向了包含 name stringage int32 的那块具体的内存区域。

2. tab 指针(指向 itab 接口表)

这是多态的核心。tab 指向了一个在运行时(或编译期优化生成)的 itab 结构。这个结构体把 Singer 接口和 *Person 类型死死地绑定在了一起。


第二层:剖析 s.tab 里的 itab 结构

我们打开 tab 指针指向的 itab 看一下,里面装的是这些东西:

Go

type itab struct {
    inter *interfacetype // 指向 Singer 接口的元数据
    _type *_type         // 指向 *Person 的类型元数据
    hash  uint32         // 从 *Person 的 _type 中拷贝过来的哈希值,用于快速断言
    _     [4]byte        // 内存对齐填充
    fun   [1]uintptr     // 存放 (*Person).Sing 方法的真实内存地址
}

这里有几个非常关键的细节:


当你调用 s.Sing() 时,底层发生了什么?

当你写下 s.Sing() 这行代码时,Go 编译器会把它翻译成底层的汇编指令,逻辑等价于:

Go

// 伪代码
s.tab.fun[0](s.data)
  1. Go 先去 s.tab 里面找到 fun 数组。
  2. 取出 fun[0](也就是 (*Person).Sing 的函数地址)。
  3. 最精妙的一步:Go 会把 s.data(也就是 &Person{} 的内存地址)作为第一个隐藏参数(Receiver 接收者),压入寄存器或栈中,传给 Sing() 函数。
  4. 然后开始执行 Sing() 内部的逻辑。

延申:如果改成值传递会怎样?

如果你的代码改成这样:

var s Singer = Person{} (注意:去掉了 &

这段代码会导致编译报错!

报错信息大致是:Person does not implement Singer (Sing method has pointer receiver)

为什么底层会报错?

因为你定义的是 func (p *Person) Sing()(指针接收者),这意味着只有 *Person 类型的方法集里有 Sing 方法,而 Person(值类型)的方法集里是没有 Sing 方法的。

当你尝试把 Person 值赋给 Singer 接口时,Go 编译器在构建底层的 itab 时,去 Person 的类型元数据里找 Sing 方法的地址准备填入 tab.fun 中,结果发现根本找不到,于是直接在编译阶段拒绝通过。

这就是为什么 Go 语言中经常强调:指针接收者实现的方法,只能赋值给接口的指针类型变量(或者说变量中存的是指针)。

无类型字面量


分享这篇文章:

上一篇
现代 C++(二):if constexpr、optional 与 variant 如何降低认知负担
下一篇
现代 C++(一):C++11/14 为什么是现代 C++ 的起点