interface 在 golang 中是一个非常重要的特性. 它相对于其它语言有很多优势:
duck typing. 大多数的静态语言需要显示的声明类型的继承关系. 而 golang 通过 interface 实现了 duck typing, 使得我们无需显示的类型继承.
不像其它实现了 duck typing 的动态语言那样, 只能在运行时才能检查到类型的转换错误. 而 golang 的 interface 特性可以让我们在编译时就能发现错误.
本文将简单分析 interface 的实现原理.
interface 的数据结构
eface 和 iface
image
eface 表示空的 interface{}, 它用两个机器字长表示, 第一个字 _type 是指向实际类型描述的指针, 第二个字 data 代表数据指针.
iface 表示至少带有一个函数的 interface, 它也用两个机器字长表示, 第一个字 tab 指向一个 itab 结构, 第二个字 data 代表数据指针.
data
data 用来保存实际变量的地址.
data 中的内容会根据实际情况变化, 因为 golang 在函数传参和赋值时是 值传递 的, 所以:
如果实际类型是一个值, 那么 interface 会保存这个值的一份拷贝. interface 会在堆上为这个值分配一块内存, 然后 data 指向它.
如果实际类型是一个指针, 那么 interface 会保存这个指针的一份拷贝. 由于 data 的长度恰好能保存这个指针的内容, 所以 data 中存储的就是指针的值. 它和实际数据指向的是同一个变量.
以 interface{} 的赋值为例:
image
上图中, i1 和 i2 是 interface,A 为要赋值给 interface 的对象.
i1 = A 将 A 的值赋值给 i1, 则 i1 中的 data 中的内容是一块新内存的地址 (0x123456), 这块内存的值从 A 拷贝.
i2 = &A 将 A 的地址赋值给 i2, 则 i2 中的 data 的值为 A 的地址, 即 0xabcdef;
itab
image
itab 表示 interface 和 实际类型的转换信息. 对于每个 interface 和实际类型, 只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itab 信息.
inter 指向对应的 interface 的类型信息.
type 和 eface 中的一样, 指向的是实际类型的描述信息 _type
fun 为函数列表, 表示对于该特定的实际类型而言, interface 中所有函数的地址.
_type
image
_type 表示类型信息. 每个类型的 _type 信息由编译器在编译时生成. 其中:
size 为该类型所占用的字节数量.
kind 表示类型的种类, 如 bool,int,float,string,struct,interface 等.
str 表示类型的名字信息, 它是一个 nameOff(int32) 类型, 通过这个 nameOff, 可以找到类型的名字字符串
灰色的 extras 对于基础类型 (如 bool,int, float 等) 是 size 为 0 的, 它为复杂的类型提供了一些额外信息. 例如为 struct 类型提供 structtype, 为 slice 类型提供 slicetype 等信息.
灰色的 ucom 对于基础类型也是 size 为 0 的, 但是对于 type Binary int 这种定义或者是其它复杂类型来说, ucom 用来存储类型的函数列表等信息.
注意 extras 和 ucom 的圆头箭头, 它表示 extras 和 ucom 不是指针, 它们的内容位于 _type 的内存空间中.
interfacetype
image
interfacetype 也并没有什么神奇的地方, 只是 _type 为 interface 类型提供的另一种信息罢了. 它包括这个 interface 所申明的所有函数信息.
interface 相关的操作
itab 中函数表(fun) 的生成
假设 interface 有 ni 个函数, struct 有 nt 个函数, 那么 itab 中的函数表生成的时间复杂度为 O(ni*nt) (遍历 interface 的所有函数, 对每次迭代都从 struct 中遍历找到匹配的函数). 但实际上编译器对此做了优化, 它将 interfacetype 中的函数列表和 uncommontype 中的函数列表都做了排序. 所以实现了 O(ni+nt) 时间复杂度的算法.
- // 生成 itab 的 funcs 的算法
- // 代码摘录自 $GOROOT/src/runtime/iface.go
- // 经过了部分修改, 只保留了最核心的逻辑
- var j = 0
- for k := 0; k <ni; k++ {
- mi := inter.methods[k]
- for ; j < nt; j++ {
- mt := t.methods[j]
- if isOk(mi, mt) {
- itab.fun[k] = mt.f
- }
- }
- }
interface 参数传递与函数调用
- type Binary uint64
- func (i Binary) String() string {
- return strconv.FormatUint(uint64(i), 10)
- }
- type Stringer interface {
- String() string
- }
- func test(s Stringer) {
- s.String()
- }
- func main() {
- b := Binary(0x123)
- test(b)
- }
在上面的代码中, golang 的参数传递过程是:
分配一块内存 p, 并且将对象 b 的内容拷贝到 p 中;
创建 iface 对象 i, 将 i.tab 赋值为 itab<Stringer, Binary>. 将 i.data 赋值为 p;
使用 i 作为参数调用 test 函数.
当 test 函数执行 s.String 时, 实际上就是在 s.tab 的 fun 中索引 (索引由编译器在编译时生成) 到 String 函数, 并且调用它.
参考资料:
- Why I like Go's interfaces
- duck typing
- Go Data Structures: Interfaces
go 源码 https://github.com/golang/go/tree/master/src/runtime
本文始发于 https://zhuanlan.zhihu.com/p/60983066
来源: http://www.jianshu.com/p/30e5cb755529