之前 看书 的时候,对于 go 的 interface 机制,对我个人而言,感觉挺新颖的,又不得其要领,心中留下了不少疑惑。
实践了一些小例子,对有了基本的了解,记录下这篇文章。
struct method
在了解 interface 之前,我们先看看 struct method 的用法,这个就比较有面向对象的感觉,fields + methods。
第 6 行中的 (r Rectangle)
的用法有点像 Lua 语法糖里的 self
,Java 里面的 this
。
1 | type Rectangle struct { |
之前看书的时候,struct method 和 interface 是一起出现的,所以心中比较疑惑这两者的关系,这回算是清楚了。
另外,这里有一个有趣的优化,我们看下生成的汇编代码,这里直接把 struct 里的 field 当做参数传给 perimeter
函数了。
1 | MOVL $0x4, AX |
PS:去掉第 5 行 go:noinline
的话,连函数调用都会被优化掉了。
Interface 抽象的是什么
struct
+ method
已经有面向对象的感觉了,那么 interface 抽象的又是什么呢?
先看一个示例,这里申明了一个叫 Shape
的 interface,其有一个 perimeter
的方法。
1 | type Shape interface { |
如果只有 Rectangle
和 Shape
的话,看起来 Shape
看起来没啥用。
如果再加一个 Triangle
,就比较好懂了,此时 Rectangle
和 Triangle
都实现了 Shape
接口。
1 | type Triangle struct { |
接下来就可以这样使用了,Rectangle
和 Triangle
都实现了 Shape
接口。
1 | var s Shape |
从我的理解而言,interface 是一种更高层次的抽象,表示具有某些能力(method)的对象,并不是特指某个对象(struct);只要某个 struct 具有 interface 定义的所有 method,则这个 struct 即自动实现了这个 interface。
有了 interface 抽象之后,我们可以只关心能力(method)而不用关心其具体的实现(struct)。
对比 C 语言常规的接口
乍一眼看 interface 的定义的时候,很像 C 语言暴露在 .h 头文件里的接口函数;但是实际上二者差距很大。
C 语言中的接口函数,更像 go package 中 export 的 function,只是公共函数而已。
interface 则是面向对象的概念,不仅仅是定义的 method 有一个隐藏的 struct 参数,而且一个 interface 变量真的会绑定一个真实的 struct。
interface 也是 go 语言里的一等公民,跟 struct 同等地位,这个跟 C 里面的函数接口就完全不是一回事了。
对比 go 语言自己的 struct
虽然 interface 和 struct 在调用 method 的使用,用法很像;但是这两也不是一回事。
interface 是更高一层的抽象,由不同的 struct 都可以实现某个接口;
而且 interface 变量只能调用 interface 申明的 method,不能调用绑定的 struct 的其他 method。
interface 的实现
里面的解释其实还是有些粗糙,看下 interface 的实现机制,就比较容易理解了。
首先,interface 是一等公民,上面例子里的 var s Shape
,实际上是构建了如下这样一个 struct。tab
表示 interface 的一些基本信息,data
则指向了一个具体的 struct。
1 | type iface struct { |
我们看下上面例子中,interface 调用过程的实际汇编代码:
1 | MOVQ $0x4, 0x38(SP) |
- 1-2 行,在栈上构建了一个 Rectangle struct
- 3-5 行,把
itab
和 struct 地址,传给convT2Inoptr
,由其构建一个堆上的 interface 变量,即 iface struct - 6 行:获取 iface 中 method perimeter 的地址,
main.(*Rectangle).perimeter
这个函数 - 7-8 行,相当于这个效果,perimeter(&struct Rectangle)
其中 convT2Inoptr
的核心代码如下,即是在堆上构建 iface
的过程。
1 | func convT2Inoptr(tab *itab, elem unsafe.Pointer) (i iface) { |
这里有一个比较有意思的地方,第 7 行 MOVQ BX, AX
中的 BX
并不是来自第 4 行的赋值,因为 go function call ABI 中,所有寄存器都是 caller-saved 的。
我们看下 convT2Inoptr
的汇编代码,可以看到它是这样处理返回值的,直接把 iface
中的两个成员返回了;按照源码的字面意思,应该只有一个返回值的。
1 | MOVQ 0x40(SP), AX |
总结
go interface 是一个挺有意思的设计,作为一等公民,跟普通类型无异,可以构建普通的 interface 变量。
另外在实现的时候,对于 iface
这种很小的 struct,go 编译器做了比较有意思的优化,直接把 struct 中的成员展开,用多个值来表示这个 struct。这样可以更充分的利用寄存器,更好的发挥 go function call ABI 的特性。