0%

go interface 机制

之前 看书 的时候,对于 go 的 interface 机制,对我个人而言,感觉挺新颖的,又不得其要领,心中留下了不少疑惑。
实践了一些小例子,对有了基本的了解,记录下这篇文章。

struct method

在了解 interface 之前,我们先看看 struct method 的用法,这个就比较有面向对象的感觉,fields + methods。
第 6 行中的 (r Rectangle) 的用法有点像 Lua 语法糖里的 self,Java 里面的 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Rectangle struct {
a, b uint64
}

//go:noinline
func (r Rectangle) perimeter() uint64 {
return (r.a * r.b) * 2
}

func main() {
s := Rectangle{4, 5}
p := s.perimeter()

fmt.Printf("perimeter: %v\n", p)
}

之前看书的时候,struct method 和 interface 是一起出现的,所以心中比较疑惑这两者的关系,这回算是清楚了。

另外,这里有一个有趣的优化,我们看下生成的汇编代码,这里直接把 struct 里的 field 当做参数传给 perimeter 函数了。

1
2
3
4
MOVL $0x4, AX
MOVL $0x5, BX
NOPW
CALL main.Rectangle.perimeter(SB)

PS:去掉第 5 行 go:noinline 的话,连函数调用都会被优化掉了。

Interface 抽象的是什么

struct + method 已经有面向对象的感觉了,那么 interface 抽象的又是什么呢?

先看一个示例,这里申明了一个叫 Shape 的 interface,其有一个 perimeter 的方法。

1
2
3
type Shape interface {
perimeter() uint64
}

如果只有 RectangleShape 的话,看起来 Shape 看起来没啥用。
如果再加一个 Triangle,就比较好懂了,此时 RectangleTriangle 都实现了 Shape 接口。

1
2
3
4
5
6
7
type Triangle struct {
a, b, c uint64
}

func (t Triangle) perimeter() uint64 {
return t.a + t.b + t.c
}

接下来就可以这样使用了,RectangleTriangle 都实现了 Shape 接口。

1
2
3
4
5
6
7
8
9
10
var s Shape
s = Rectangle{4, 5}
p := s.perimeter()

fmt.Printf("Rectangle perimeter: %v\n", p)

s = Triangle{3, 4, 5}
p = s.perimeter()

fmt.Printf("Triangle perimeter: %v\n", p)

从我的理解而言,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
2
3
4
type iface struct {
tab *itab
data unsafe.Pointer
}

我们看下上面例子中,interface 调用过程的实际汇编代码:

1
2
3
4
5
6
7
8
MOVQ $0x4, 0x38(SP)
MOVQ $0x5, 0x40(SP)
LEAQ go.itab.main.Rectangle,main.Shape(SB), AX
LEAQ 0x38(SP), BX
CALL runtime.convT2Inoptr(SB)
MOVQ 0x18(AX), CX
MOVQ BX, AX
CALL CX
  1. 1-2 行,在栈上构建了一个 Rectangle struct
  2. 3-5 行,把 itab 和 struct 地址,传给 convT2Inoptr,由其构建一个堆上的 interface 变量,即 iface struct
  3. 6 行:获取 iface 中 method perimeter 的地址,main.(*Rectangle).perimeter 这个函数
  4. 7-8 行,相当于这个效果,perimeter(&struct Rectangle)

其中 convT2Inoptr 的核心代码如下,即是在堆上构建 iface 的过程。

1
2
3
4
5
6
7
8
func convT2Inoptr(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
x := mallocgc(t.size, t, false)
memmove(x, elem, t.size)
i.tab = tab
i.data = x
return
}

这里有一个比较有意思的地方,第 7 行 MOVQ BX, AX 中的 BX 并不是来自第 4 行的赋值,因为 go function call ABI 中,所有寄存器都是 caller-saved 的。
我们看下 convT2Inoptr 的汇编代码,可以看到它是这样处理返回值的,直接把 iface 中的两个成员返回了;按照源码的字面意思,应该只有一个返回值的。

1
2
3
4
5
MOVQ 0x40(SP), AX
MOVQ 0x18(SP), BX
MOVQ 0x30(SP), BP
ADDQ $0x38, SP
RET

总结

go interface 是一个挺有意思的设计,作为一等公民,跟普通类型无异,可以构建普通的 interface 变量。

另外在实现的时候,对于 iface 这种很小的 struct,go 编译器做了比较有意思的优化,直接把 struct 中的成员展开,用多个值来表示这个 struct。这样可以更充分的利用寄存器,更好的发挥 go function call ABI 的特性。