vault backup: 2026-06-07 12:14:39
This commit is contained in:
@@ -1,239 +1,191 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go原理深入
|
||||
- defer原理
|
||||
tags: [go, golang, go-principle, interface]
|
||||
create time: 2026-06-07 15:40
|
||||
---
|
||||
|
||||
# interface原理
|
||||
# Interface 底层原理
|
||||
|
||||
go语言并非传统意义上的面向对象的语言,他不像Java或者c++一样有类,继承等一些特性,但是我们也可以借助go语言中的struct和interface来实现这种面向对象的编程。在前面的基础章节我们了解到go语言中interface其实就是一组方法的声明,任何类型的对象实现了接口的全部方法就是这个接口的一个实现。这一章节我们主要分析一下interface的底层实现。
|
||||
## 概述
|
||||
|
||||
## interface的底层原理
|
||||
本文从 runtime 源码角度解析 Go 接口的两种内部表示:空接口(eface)和非空接口(iface),以及 itab 的创建、缓存机制。理解接口底层能让你写出更高效的类型断言代码,也能解释为什么 `nil interface` 不等于 `interface(nil)`。
|
||||
|
||||
### 空接口interface{}
|
||||
> [!question] ❓ 思考
|
||||
> 为什么 `var p *int = nil; var i io.Writer = p` 中 `i != nil`?空接口 `interface{}` 和非空接口在内存布局上有什么本质区别?itab 为什么需要缓存?
|
||||
|
||||
没有定义任何方法的接口为空接口,空接口可以接收任意数据类型,就是说可以将任意类型的数据赋值给一个空接口,空接口的结构定义位于`src/runtime/runtime2.go`,定义如下:
|
||||
## 正文
|
||||
|
||||
### 一、空接口:eface
|
||||
|
||||
没有任何方法声明的接口就是空接口 `interface{}`:
|
||||
|
||||
```go
|
||||
type eface struct {
|
||||
_type *_type
|
||||
data unsafe.Pointer
|
||||
_type *_type // 动态类型元数据
|
||||
data unsafe.Pointer // 动态值(指向数据的指针)
|
||||
}
|
||||
```
|
||||
|
||||
\_type:指向接口的动态类型元数据,即接口变量的类型
|
||||
两个字段各 8 字节,共 16 字节。赋值前后对比:
|
||||
|
||||
data:指向接口的动态值,data是一个指向变量本身的指针
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Before["var e interface{}<br/>_type=nil, data=nil"] --> After["e = 42<br/>_type→int元数据, data→&42"]
|
||||
style Before fill:#ffebee
|
||||
style After fill:#e8f5e9
|
||||
```
|
||||
|
||||
#### \_type是什么
|
||||
|
||||
 \_type 是 go 里面所有类型的一个抽象,里面包含了类型的大小,哈希,对齐以及k类型编号等信息,决定了data如何解释和操作,Go语言中几乎所有的数据结构都可以抽象成`_type`。关于\_type的定义在源文件`src/runtime/type.go`,具体定义如下:
|
||||
#### _type:类型的"身份证"
|
||||
|
||||
```go
|
||||
type _type struct {
|
||||
size uintptr // 数据类型占用的空间大小
|
||||
ptrdata uintptr // 前缀持有所有指针的内存大小
|
||||
hash uint32 // 类型的hash值
|
||||
tflag tflag // 信息标志
|
||||
align uint8 // 这种类型在内存中的对齐方式
|
||||
fieldAlign uint8
|
||||
kind uint8 // 类型编号
|
||||
equal func(unsafe.Pointer, unsafe.Pointer) bool // 类型的比较函数
|
||||
gcdata *byte
|
||||
str nameOff
|
||||
ptrToThis typeOff
|
||||
size uintptr // 类型大小
|
||||
ptrdata uintptr // 前缀含指针的字节数
|
||||
hash uint32 // 类型的 hash 值
|
||||
tflag tflag // 类型标志
|
||||
align uint8 // 内存对齐
|
||||
fieldAlign uint8
|
||||
kind uint8 // 类型编号(struct/function/interface...)
|
||||
equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较函数
|
||||
gcdata *byte
|
||||
str nameOff
|
||||
ptrToThis typeOff
|
||||
}
|
||||
```
|
||||
|
||||
什么是动态类型和动态值呢,举个例子
|
||||
`_type` 是 Go 所有类型的抽象基类——int、string、struct 等所有类型都对应一个 `_type` 实例。
|
||||
|
||||
```go
|
||||
package main
|
||||
### 二、非空接口:iface + itab
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Apple struct {
|
||||
PhoneName string
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
a := Apple{PhoneName: "apple"}
|
||||
var efc interface{}
|
||||
efc = a
|
||||
fmt.Println(efc)
|
||||
}
|
||||
```
|
||||
|
||||
这里在第12行定义了一个接口类型实例efc,此时还未对efc赋值,它的结构如下图所示:
|
||||
|
||||

|
||||
|
||||
在第13行,对efc赋值了一个Apple类型的变量之后,其底层结构表现如下图所示:
|
||||
|
||||

|
||||
|
||||
其中\_type指针指向a变量的类型元数据,data指针指向a变量的值
|
||||
|
||||
### 非空接口
|
||||
|
||||
包含方法列表的接口就是非空接口,例如下面定义的接口Phone就是一个非空接口:
|
||||
|
||||
```go
|
||||
type Phone interface {
|
||||
Call()
|
||||
}
|
||||
```
|
||||
|
||||
非空接口的底层实现按与空接口有所不同,因为其多了方法列表,在底层实现中显然我们需要有地方来存储方法列表,非空接口的结构定义位于`src/runtime/runtime2.go`,定义如下:
|
||||
包含方法列表的接口需要额外的结构来存储方法地址:
|
||||
|
||||
```go
|
||||
type iface struct {
|
||||
tab *itab
|
||||
data unsafe.Pointer
|
||||
tab *itab // 接口类型信息 + 动态类型信息 + 方法地址
|
||||
data unsafe.Pointer // 动态值
|
||||
}
|
||||
```
|
||||
|
||||
data:指向接口的动态值,这里跟空接口一样
|
||||
|
||||
tab:指向一个itab的结构,itab结构里面存储值接口要求的方法列表和 data对应动态类型信息
|
||||
|
||||
 下面看一下itab的结构定义,itab结构定义在`src/runtime/runtime2.go`,定义如下:
|
||||
|
||||
```go
|
||||
type itab struct {
|
||||
inter *interfacetype
|
||||
_type *_type
|
||||
hash uint32 // copy of _type.hash. Used for type switches.
|
||||
_ [4]byte
|
||||
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
|
||||
inter *interfacetype // 接口本身的描述(方法列表)
|
||||
_type *_type // 实现类型的描述
|
||||
hash uint32 // _type.hash 的副本,用于类型 switch
|
||||
_ [4]byte
|
||||
fun [1]uintptr // 可变长数组:接口方法的实际地址
|
||||
}
|
||||
```
|
||||
|
||||
inter :指向interfacetype结构的指针,interfacetype结构记录了这个接口类型的描述信息,主要是接口的方法列表
|
||||
|
||||
\_type:实际类型的指针,指向\_type结构,\_type结构保存了接口的动态类型信息,跟空接口的\_type一样,即赋值给这个接口的具体类型信息的元数据
|
||||
|
||||
hash:该类型的hash值,itab中的hash和itab.\_type中的hash相等,其实是从itab.\_type中拷贝出来的,目的是用于快速判断类型是否相等
|
||||
|
||||
fun:fun是一个指针数组,里面保存了实现了该接口的实际类型的方法(只包含接口中的方法)地址,这些方法地址实际上是从interfacetype结构中的mhdr拷贝出来的,为了在调用的时候快速定位到方法。如果该接口对应的动态类型没有实现接口的所有方法,那么itab.fun\[0]=0,表示断言失败,该类型不能赋值给该接口
|
||||
|
||||
interfacetype 保存了接口自身的元信息,下面看一下interfacetype结构
|
||||
用一个具体例子展示完整结构:
|
||||
|
||||
```go
|
||||
type interfacetype struct {
|
||||
typ _type // 类型信息
|
||||
pkgpath name // 包路径
|
||||
mhdr []imethod // 接口的方法列表
|
||||
}
|
||||
```
|
||||
|
||||
这里主要关注的是mhdr这个字段,定义的接口的方法里表就保存在mhdr数组里
|
||||
|
||||
下面还是通过例子看一下,赋值一个非空接口对应的底层结构变化
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Apple struct {
|
||||
PhoneName string
|
||||
}
|
||||
|
||||
func (a Apple) Call() {
|
||||
fmt.Printf("%s有打电话功能\n", a.PhoneName)
|
||||
}
|
||||
|
||||
func (a Apple) SendMessage() {
|
||||
fmt.Printf("%s有发短信功能\n", a.PhoneName)
|
||||
}
|
||||
|
||||
func (a Apple) SendEmail() {
|
||||
fmt.Printf("%s有发邮件功能\n", a.PhoneName)
|
||||
}
|
||||
|
||||
type Phone interface {
|
||||
Call()
|
||||
SendMessage()
|
||||
Call()
|
||||
SendMessage()
|
||||
}
|
||||
|
||||
func main() {
|
||||
a := Apple{PhoneName: "apple"}
|
||||
var ifc Phone
|
||||
ifc = a
|
||||
fmt.Println(ifc)
|
||||
type Apple struct { PhoneName string }
|
||||
func (a Apple) Call() {}
|
||||
func (a Apple) SendMessage() {}
|
||||
|
||||
var ifc Phone = Apple{PhoneName: "iphone"}
|
||||
```
|
||||
|
||||
在程序第28行,赋值之前,ifc的机构如下图所:
|
||||
|
||||

|
||||
|
||||
在第29行,给ifc赋值一个包含方法的结构体a之后,ifc的结构如下图:
|
||||
|
||||

|
||||
|
||||
赋值过程中,data指针其实还是和空接口一样指向具体类型值,这里指向变量a。tab指针则是指向itab这个结构体,itab结构创建的创建主要就分为3部分:
|
||||
|
||||
1. \_type字段保存接口的动态类型信息,本例中,\_type指针指向Apple类型的元数据
|
||||
|
||||
2. inter保存接口本身的一些信息,这里重要处理方法列表,本质上其实是求接口类型(Phone)和具体类型(Apple)的方法列表的交集,将具体类型(Apple)这部分交集的方法地址保存到interfacetype的mhdr数组中,假设具体类型(Apple)没有实现接口(Phone),那么这里mhdr数组将不包含任何方法的指针
|
||||
|
||||
3. 最后再将mhdr数组中的方法地址拷贝到itab的fun数组中,方便调用方法的时候快速找到方法地址,如果具体类型(Apple)没有实现接口(Phone),那么这里itab.fun\[0]=0
|
||||
|
||||
#### itab缓存
|
||||
|
||||
通过前面的分析我们知道,在给一个非空接口赋值的时候,itab里面主要是保存具体类型的类型元数据和方法列表,但是我们在给接口赋值的时候,我们可以赋值多个类型相同的动态类型,比如我们可以由如下代码:
|
||||
|
||||
```go
|
||||
var ifc Phone
|
||||
a := Apple{PhoneName: "apple1"}
|
||||
b := Apple{PhoneName: "apple2"}
|
||||
C := Apple{PhoneName: "apple3"}
|
||||
ifc = a
|
||||
ifc = b
|
||||
ifc = c
|
||||
```mermaid
|
||||
graph TB
|
||||
ifc["iface<br/>tab → itab | data → &Apple"]
|
||||
|
||||
itab["itab"] --> inter["interfacetype<br/>Phone.Call, Phone.SendMessage"]
|
||||
itab --> atype["_type<br/>Apple 的元数据"]
|
||||
itab --> fun["fun[0]=Call_addr<br/>fun[1]=SendMessage_addr"]
|
||||
|
||||
apple["Apple 实例<br/>PhoneName='iphone'"] -.data指向.-> ifc
|
||||
|
||||
style ifc fill:#e3f2fd
|
||||
style itab fill:#fff9c4
|
||||
style inter fill:#e8f5e9
|
||||
style apple fill:#fce4ec
|
||||
```
|
||||
|
||||
同类型的接口多次赋值,虽然具体类型的值不同,但是他们的类型相同,方法列表也相同,显然这个itab结构体是可以被复用的,如果我们每次都创建一个新的itab的话,性能无疑会大大下降。所以可以把用到的itab结构体缓存起来,每个非空的interface的接口类型和具体类型就可以唯一确定一个类型的itab。
|
||||
> [!note] 📝 源码要点
|
||||
> itab.fun 是一个**可变长数组**。它保存的不是接口定义中的方法地址,而是具体类型(Apple)中对应方法的实际地址。这是通过求接口方法列表和具体类型方法列表的交集得到的。
|
||||
|
||||
##### itabTable
|
||||
### 三、Itab 缓存:itabTable
|
||||
|
||||
go语言采用itabTable这个结构来缓存所有的itab结构,itabTable的结构定义在`src/runtime/iface.go`,定义如下:
|
||||
每次给接口赋值都要查找或创建 itab 吗?不会。Go 用哈希表缓存所有已创建的 itab:
|
||||
|
||||
```go
|
||||
type itabTableType struct {
|
||||
size uintptr // entries数组的长度
|
||||
count uintptr // 当前数组中实际itab的数量
|
||||
entries [itabInitSize]*itab // 哈希表
|
||||
size uintptr
|
||||
count uintptr
|
||||
entries [itabInitSize]*itab // 哈希表,2^11 = 2048 个槽位
|
||||
}
|
||||
```
|
||||
|
||||
itabTable实际用来存储itab结构的其实是这个entries 结构,entries 是一个hash表,key为接口类型与实际类型分别哈希后的异或值
|
||||
查找流程:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start["给接口赋值"] --> Hash{"itab 已在缓存?"}
|
||||
Hash -->|是| Return["直接复用 existing itab"]
|
||||
Hash -->|否| Create["创建新 itab:<br/>1. 填充 inter 和 _type<br/>2. 求方法交集 → fun[]<br/>3. CAS 插入缓存"]
|
||||
Create --> Insert{"哈希冲突?"}
|
||||
Insert -->|是| Quadratic["二次寻址法找空位"]
|
||||
Insert -->|否| Store["存入计算出的槽位"]
|
||||
Quadratic --> Store
|
||||
Store --> Done["完成"]
|
||||
Return --> Done
|
||||
style Return fill:#e8f5e9
|
||||
style Create fill:#fff9c4
|
||||
```
|
||||
|
||||
哈希 key 的计算方式:
|
||||
|
||||
```go
|
||||
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
|
||||
func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
|
||||
return uintptr(inter.typ.hash ^ typ.hash)
|
||||
}
|
||||
```
|
||||
|
||||
所以key为整形,所以在实现的时候,go语言采用了空间换时间的思想,通过一个数组来实现,数组的每个元素为itab指针,其结构如下:
|
||||
同类型的多次赋值只创建一个 itab:
|
||||
|
||||

|
||||
```go
|
||||
var ifc Phone
|
||||
ifc = Apple{...} // 创建 itab,缓存起来
|
||||
ifc = Apple{...} // 直接从缓存取
|
||||
ifc = Nokia{...} // 为 Nokia 创建新的 itab
|
||||
```
|
||||
|
||||
我们在查找一个itab是否存在的时候,
|
||||
### 四、常见陷阱:nil interface ≠ interface(nil)
|
||||
|
||||
1. 先计算接口类型的哈希值hash1和实际类型的哈希值hash2
|
||||
```go
|
||||
var p *int = nil
|
||||
var i io.Writer = p // i 的 data 是 nil,但 tab 不为 nil
|
||||
fmt.Println(i == nil) // false!
|
||||
```
|
||||
|
||||
2. hash1与hash2做异或运算得到最终哈希值hash
|
||||
原因:接口等于 nil 的判定条件是 `tab == nil && data == nil`。虽然 `data` 是 nil,但 `tab` 指向了一个有效的 itab(其中 fun 全为零),所以 `i != nil`。
|
||||
|
||||
3. 在entries数组中找到下标为hash的位置
|
||||
```go
|
||||
var i2 io.Writer = nil // 这才是真正的 nil interface
|
||||
fmt.Println(i2 == nil) // true
|
||||
```
|
||||
|
||||
4. 如果能查询到对应的itab指针(这里需要比较接口类型和实际类型,因为可能出现产生hash冲突,槽位被占用的情况),就直接拿来使用。若没有就要再创建,然后添加到itabTable中。
|
||||
> [!warning] ⚠️ 面试高频
|
||||
> 这个知识点是 Go 面试中最常考的接口相关题目之一。核心结论:**接口由 (tab, data) 两个字段组成,只有两者都为 nil 时接口才等于 nil。**
|
||||
|
||||
对于hash冲突问题,采用的是开放地址法,根据计算的空位发现槽位被占用,则采用**二次寻址法**在数组后面寻找空位插入
|
||||
### 五、性能影响
|
||||
|
||||
1. **接口赋值有轻微开销**:需要查找/创建 itab,但命中缓存后几乎无额外成本
|
||||
2. **接口比较比类型比较慢**:需要先比较 `_type.hash`,再比较完整 `_type`
|
||||
3. **避免不必要的接口转换**:频繁的类型断言会增加运行时开销
|
||||
|
||||
## 小结
|
||||
|
||||
- 空接口 `eface` 是 `(_type, data)` 二元组;非空接口 `iface` 多了 `itab`
|
||||
- itab 包含接口类型、实现类型和方法地址,创建后缓存到 itabTable
|
||||
- 接口判 nil 要看 tab 和 data 是否同时为 nil
|
||||
- 同类型的接口赋值复用同一个 itab,首次创建有开销,后续命中缓存
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hzh/GolangStar/Go语言基础/Go语言接口]] — 接口的基础用法与隐式实现
|
||||
- [[hzh/GolangStar/Go语言原理/interface原理]] — (同名文件)
|
||||
- [[hzh/GolangStar/Go面试题库/Interface面试题]] — Interface 相关高频面试题
|
||||
|
||||
Reference in New Issue
Block a user