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 官方为了统一 runtime 和 reflect 包底层的类型系统,将它重构到了 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 连在一起,打包成一段紧凑的字节流。
这段字节流在内存中的排布规则大致如下:
- Flag(标志位):1 个字节。其中有一位专门用来标记**“这个字段有没有 Tag”**。
- 名字的长度:动态变长字节(Varint)。
- 名字本身:例如
"Age"的 ASCII 码。 - Tag 的长度:(只有当 Flag 标记有 Tag 时才存在)。
- Tag 本身:例如
`json:"age" valid:"required"`的 ASCII 码。
当你调用 reflect.TypeOf(obj).Field(0) 时,reflect 包底层的逻辑就是:
- 找到底层的
abi.StructField。 - 读取它的
Name指针指向的字节流。 - 检查 Flag 看看有没有 Tag。
- 如果有,就越过字段名,把后面的 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 string 和 age 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 方法的真实内存地址
}
这里有几个非常关键的细节:
_type字段:请特别注意,这里记录的类型是*Person(指针类型),而不是Person(值类型)。Go 语言的类型系统中,T和*T是两个完全不同的类型,它们有各自独立的_type元数据。fun数组:在这个例子中,Singer只有一个方法Sing()。所以fun[0]里面存储的,就是你代码中func (p *Person) Sing()这个函数的机器码在内存中的入口地址。
当你调用 s.Sing() 时,底层发生了什么?
当你写下 s.Sing() 这行代码时,Go 编译器会把它翻译成底层的汇编指令,逻辑等价于:
Go
// 伪代码
s.tab.fun[0](s.data)
- Go 先去
s.tab里面找到fun数组。 - 取出
fun[0](也就是(*Person).Sing的函数地址)。 - 最精妙的一步:Go 会把
s.data(也就是&Person{}的内存地址)作为第一个隐藏参数(Receiver 接收者),压入寄存器或栈中,传给Sing()函数。 - 然后开始执行
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 语言中经常强调:指针接收者实现的方法,只能赋值给接口的指针类型变量(或者说变量中存的是指针)。