我们已经看过了 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