你已经知道:
go
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "汪汪"
}
这里:
txt
Animal = 接口
Dog = 结构体
Speak = 方法
那么:
Go 到底是怎么知道:
go
var a Animal = Dog{}
里面的 Dog 实现了 Animal?
又是怎么找到:
go
a.Speak()
应该调用哪个函数的?
答案:
就是:
txt
itab
一、先看这段代码
go
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "汪汪"
}
func main() {
var a Animal
a = Dog{}
fmt.Println(a.Speak())
}
输出:
txt
汪汪
二、问题来了
变量:
go
a
类型是:
go
Animal
但是:
真正装进去的是:
go
Dog{}
那么:
执行:
go
a.Speak()
的时候:
Go 怎么知道:
应该去调用:
go
Dog.Speak()
而不是:
go
Cat.Speak()
呢?
三、interface 底层结构
前面讲过:
interface 本质:
txt
(Type, Value)
例如:
go
var a Animal = Dog{}
底层:
大概是:
txt
T = Dog
V = Dog{}
四、但仅仅有类型还不够
因为:
Go 还需要知道:
txt
这个类型实现了哪些接口方法
例如:
txt
Dog
↓
Speak()
↓
对应哪个函数地址
五、于是出现了 itab
itab:
全称:
txt
interface table
翻译:
txt
接口方法表
六、itab 是干什么的
一句话:
txt
记录
某个具体类型
如何实现某个接口
七、画图理解
例如:
go
var a Animal = Dog{}
底层:
大概这样:
txt
a
│
├── itab
│
└── data
八、data
data:
保存:
txt
真正的数据
也就是:
txt
Dog{}
九、itab
itab:
保存:
txt
接口信息
+
具体类型信息
+
方法地址
十、对应到你的代码
接口类型
go
type Animal interface {
Speak() string
}
对应:
txt
Animal
具体类型
go
type Dog struct{}
对应:
txt
Dog
方法实现
go
func (d Dog) Speak() string
对应:
txt
Dog.Speak()
十一、itab 里面存什么
大概可以理解成:
txt
itab
├── interface type
├── concrete type
└── method table
具体:
txt
itab
├── Animal
├── Dog
└── Dog.Speak()
十二、画成表
| 项目 | 对应 |
|---|---|
| interface type | Animal |
| concrete type | Dog |
| method | Dog.Speak |
十三、执行 a.Speak() 时发生什么
代码:
go
a.Speak()
Go:
先找到:
txt
a.itab
然后:
在里面找到:
txt
Speak 对应的函数地址
最终调用:
go
Dog.Speak()
十四、整个过程
txt
a.Speak()
↓
找到 itab
↓
找到 Speak 函数地址
↓
执行 Dog.Speak()
↓
返回 汪汪
十五、为什么接口调用比普通调用慢一点
普通调用:
go
dog.Speak()
编译时:
就知道:
txt
调用哪个函数
接口调用:
go
a.Speak()
需要:
txt
先查 itab
再跳转函数
所以:
会多一步。
十六、再举一个例子
go
type Cat struct{}
func (c Cat) Speak() string {
return "喵喵"
}
此时:
go
var a Animal = Cat{}
底层:
变成:
txt
itab
├── Animal
├── Cat
└── Cat.Speak()
十七、为什么不用提前写 implements
Java:
java
class Dog implements Animal
Go:
不用。
因为:
Go 编译器:
会自动检查:
txt
Dog 有没有实现 Animal 的全部方法
如果有:
自动生成:
txt
Animal-Dog 的 itab
十八、编译器什么时候创建 itab
当你写:
go
var a Animal = Dog{}
时。
编译器发现:
txt
Dog 实现了 Animal
于是:
创建:
txt
Animal ↔ Dog
对应的 itab。
十九、为什么接口断言能成功
例如:
go
var a Animal = Dog{}
执行:
go
dog := a.(Dog)
Go:
会看:
txt
itab 里的 concrete type
发现:
txt
Dog
于是:
断言成功。
二十、结合 interface = (Type, Value) 再理解
很多教程说:
txt
interface = (Type, Value)
其实:
更准确:
应该理解成:
txt
interface
├── itab
└── data
其中:
data
保存:
txt
真正的数据
例如:
txt
Dog{}
itab
保存:
txt
Dog 的类型
Animal 的信息
Dog.Speak 的地址
二十一、最终完整图(重点)
txt
var a Animal = Dog{}
底层:
txt
a
│
├── itab
│ ├── interface type : Animal
│ ├── concrete type : Dog
│ └── method table
│ └── Speak → Dog.Speak
│
└── data
└── Dog{}
二十二、最后一句总结(必须记住)
itab 本质:
txt
接口方法映射表
作用:
txt
记录
某个具体类型
如何实现某个接口
在你的例子里:
txt
Animal ← 接口类型
Dog ← 具体类型
Dog.Speak ← 方法实现
Go 会生成:
txt
itab
├── Animal
├── Dog
└── Speak → Dog.Speak
当执行:
go
a.Speak()
时:
实际上:
就是:
txt
通过 itab
找到 Dog.Speak()
再调用
所以:
txt
interface 能装不同类型
却还能正确调用对应方法
核心秘密:
就是:
txt
itab + data