192 lines
6.1 KiB
Markdown
192 lines
6.1 KiB
Markdown
---
|
||
tags: [go, golang, go-principle, interface]
|
||
create time: 2026-06-07 15:40
|
||
---
|
||
|
||
# Interface 底层原理
|
||
|
||
## 概述
|
||
|
||
本文从 runtime 源码角度解析 Go 接口的两种内部表示:空接口(eface)和非空接口(iface),以及 itab 的创建、缓存机制。理解接口底层能让你写出更高效的类型断言代码,也能解释为什么 `nil interface` 不等于 `interface(nil)`。
|
||
|
||
> [!question] ❓ 思考
|
||
> 为什么 `var p *int = nil; var i io.Writer = p` 中 `i != nil`?空接口 `interface{}` 和非空接口在内存布局上有什么本质区别?itab 为什么需要缓存?
|
||
|
||
## 正文
|
||
|
||
### 一、空接口:eface
|
||
|
||
没有任何方法声明的接口就是空接口 `interface{}`:
|
||
|
||
```go
|
||
type eface struct {
|
||
_type *_type // 动态类型元数据
|
||
data unsafe.Pointer // 动态值(指向数据的指针)
|
||
}
|
||
```
|
||
|
||
两个字段各 8 字节,共 16 字节。赋值前后对比:
|
||
|
||
```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:类型的"身份证"
|
||
|
||
```go
|
||
type _type struct {
|
||
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` 实例。
|
||
|
||
### 二、非空接口:iface + itab
|
||
|
||
包含方法列表的接口需要额外的结构来存储方法地址:
|
||
|
||
```go
|
||
type iface struct {
|
||
tab *itab // 接口类型信息 + 动态类型信息 + 方法地址
|
||
data unsafe.Pointer // 动态值
|
||
}
|
||
|
||
type itab struct {
|
||
inter *interfacetype // 接口本身的描述(方法列表)
|
||
_type *_type // 实现类型的描述
|
||
hash uint32 // _type.hash 的副本,用于类型 switch
|
||
_ [4]byte
|
||
fun [1]uintptr // 可变长数组:接口方法的实际地址
|
||
}
|
||
```
|
||
|
||
用一个具体例子展示完整结构:
|
||
|
||
```go
|
||
type Phone interface {
|
||
Call()
|
||
SendMessage()
|
||
}
|
||
|
||
type Apple struct { PhoneName string }
|
||
func (a Apple) Call() {}
|
||
func (a Apple) SendMessage() {}
|
||
|
||
var ifc Phone = Apple{PhoneName: "iphone"}
|
||
```
|
||
|
||
```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
|
||
```
|
||
|
||
> [!note] 📝 源码要点
|
||
> itab.fun 是一个**可变长数组**。它保存的不是接口定义中的方法地址,而是具体类型(Apple)中对应方法的实际地址。这是通过求接口方法列表和具体类型方法列表的交集得到的。
|
||
|
||
### 三、Itab 缓存:itabTable
|
||
|
||
每次给接口赋值都要查找或创建 itab 吗?不会。Go 用哈希表缓存所有已创建的 itab:
|
||
|
||
```go
|
||
type itabTableType struct {
|
||
size uintptr
|
||
count uintptr
|
||
entries [itabInitSize]*itab // 哈希表,2^11 = 2048 个槽位
|
||
}
|
||
```
|
||
|
||
查找流程:
|
||
|
||
```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 {
|
||
return uintptr(inter.typ.hash ^ typ.hash)
|
||
}
|
||
```
|
||
|
||
同类型的多次赋值只创建一个 itab:
|
||
|
||
```go
|
||
var ifc Phone
|
||
ifc = Apple{...} // 创建 itab,缓存起来
|
||
ifc = Apple{...} // 直接从缓存取
|
||
ifc = Nokia{...} // 为 Nokia 创建新的 itab
|
||
```
|
||
|
||
### 四、常见陷阱:nil interface ≠ interface(nil)
|
||
|
||
```go
|
||
var p *int = nil
|
||
var i io.Writer = p // i 的 data 是 nil,但 tab 不为 nil
|
||
fmt.Println(i == nil) // false!
|
||
```
|
||
|
||
原因:接口等于 nil 的判定条件是 `tab == nil && data == nil`。虽然 `data` 是 nil,但 `tab` 指向了一个有效的 itab(其中 fun 全为零),所以 `i != nil`。
|
||
|
||
```go
|
||
var i2 io.Writer = nil // 这才是真正的 nil interface
|
||
fmt.Println(i2 == nil) // true
|
||
```
|
||
|
||
> [!warning] ⚠️ 面试高频
|
||
> 这个知识点是 Go 面试中最常考的接口相关题目之一。核心结论:**接口由 (tab, data) 两个字段组成,只有两者都为 nil 时接口才等于 nil。**
|
||
|
||
### 五、性能影响
|
||
|
||
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 相关高频面试题
|