我们已经看过了 iface
和 eface
的源码,知道 iface
最重要的是 itab
和 _type
。
为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。
来看一个示例代码:
|
|
执行命令:
|
|
得到 main 函数的汇编代码如下:
|
|
我们从第 10 行开始看,如果不理解前面几行汇编代码的话,可以回去看看公众号前面两篇文章,这里我就省略了。
汇编行数 | 操作 |
---|---|
10-14 | 构造调用 runtime.convT2I64(SB) 的参数 |
我们来看下这个函数的参数形式:
|
|
convT2I64
会构造出一个 inteface
,也就是我们的 Person
接口。
第一个参数的位置是 (SP)
,这里被赋上了 go.itab."".Student,"".Person(SB)
的地址。
我们从生成的汇编找到:
|
|
size=40
大小为40字节,回顾一下:
|
|
把每个字段的大小相加,itab
结构体的大小就是 40 字节。上面那一串数字实际上是 itab
序列化后的内容,注意到大部分数字是 0,从 24 字节开始的 4 个字节 da 9f 20 d4
实际上是 itab
的 hash
值,这在判断两个类型是否相同的时候会用到。
下面两行是链接指令,简单说就是将所有源文件综合起来,给每个符号赋予一个全局的位置值。这里的意思也比较明确:前8个字节最终存储的是 type."".Person
的地址,对应 itab
里的 inter
字段,表示接口类型;8-16 字节最终存储的是 type."".Student
的地址,对应 itab
里 _type
字段,表示具体类型。
第二个参数就比较简单了,它就是数字 18
的地址,这也是初始化 Student
结构体的时候会用到。
汇编行数 | 操作 |
---|---|
15 | 调用 runtime.convT2I64(SB) |
具体看下代码:
|
|
这块代码比较简单,把 tab
赋给了 iface
的 tab
字段;data
部分则是在堆上申请了一块内存,然后将 elem
指向的 18
拷贝过去。这样 iface
就组装好了。
汇编行数 | 操作 |
---|---|
17 | 把 i.tab 赋给 CX |
18 | 把 i.data 赋给 AX |
19-21 | 检测 i.tab 是否是 nil,如果不是的话,把 CX 移动 8 个字节,也就是把 itab 的 _type 字段赋给了 CX,这也是接口的实体类型,最终要作为 fmt.Println 函数的参数 |
后面,就是调用 fmt.Println
函数及之前的参数准备工作了,不再赘述。
这样,我们就把一个 interface
的构造过程说完了。
【引申1】
如何打印出接口类型的 Hash
值?
这里参考曹大神翻译的一篇文章,参考资料里会写上。具体做法如下:
|
|
定义了一个山寨版
的 iface
和 itab
,说它山寨
是因为 itab
里的一些关键数据结构都不具体展开了,比如 _type
,对比一下正宗的定义就可以发现,但是山寨版
依然能工作,因为 _type
就是一个指针而已嘛。
在 main
函数里,先构造出一个接口对象 qcrao
,然后强制类型转换,最后读取出 hash
值,非常妙!你也可以自己动手试一下。
运行结果:
|
|
值得一提的是,构造接口 qcrao
的时候,即使我把 age
写成其他值,得到的 hash
值依然不变的,这应该是可以预料的,hash
值只和他的字段、方法相关。
参考资料 #
【曹大神翻译的文章,非常硬核】http://xargin.com/go-and-interface/#reconstructing-an-itab-from-an-executable