---
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{}
_type=nil, data=nil"] --> After["e = 42
_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
tab → itab | data → &Apple"]
itab["itab"] --> inter["interfacetype
Phone.Call, Phone.SendMessage"]
itab --> atype["_type
Apple 的元数据"]
itab --> fun["fun[0]=Call_addr
fun[1]=SendMessage_addr"]
apple["Apple 实例
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:
1. 填充 inter 和 _type
2. 求方法交集 → fun[]
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 相关高频面试题