理解Go interface的两种底层实现:iface和eface
2018-11-06
Go语言interface的运行时实现的源码位于$GOROOT/src/runtime/runtime2.go
中。
在Go的不同版本中,interface的实现可能会有不同,但整体结构变化不大,本文基于Go 1.17。
1.两类接口的运行时实现 #
可以在runtime/runtime2.go
中找到Go的接口类型变量在运行时的表示,如下是iface
和eface
两个结构体:
1// $GOROOT/src/runtime/runtime2.go
2type iface struct {
3 tab *itab
4 data unsafe.Pointer
5}
6
7type eface struct {
8 _type *_type
9 data unsafe.Pointer
10}
我们知道Go的interface两类,一类是拥有方法集(MethodSet)的接口;另一个类是没有方法的空接口,也就是interface{}。 iface和eface就分别在运行时表示这两个类接口类型的变量:
- iface - 表示拥有方法的接口类型变量
- eface - 表示m没有方法的空接口(empty interfac)类型变量,即interface{}类型的变量
1.1 iface struct #
先看一下iface struct,它有两个指针字段tab
和data
。
1// $GOROOT/src/runtime/runtime2.go
2type iface struct {
3 tab *itab
4 data unsafe.Pointer
5}
我们后边统一把实现了接口的类型成为具体类型。实际使用时,一般会把具体类型的变量赋值给接口类型变量。
data字段"指向"当前被赋值给接口类型变量的具体类型变量的值。
tab字段不仅被用来存储接口本身的信息(例如接口的类型信息、方法集信息等),还被用来存储具体类型所实现的信息。tab字段是一个itab
struct的指针。
itab这个struct的定义如下:
1// $GOROOT/src/runtime/runtime2.go
2type itab struct {
3 inter *interfacetype
4 _type *_type
5 hash uint32 // copy of _type.hash. Used for type switches.
6 _ [4]byte
7 fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
8}
itab的inter字段存储就是接口本身的信息。inter字段是一个interfacetype
struct的指针,它的定义如下:
1// $GOROOT/src/runtime/runtime2.go
2type interfacetype struct {
3 typ _type
4 pkgpath name
5 mhdr []imethod
6}
可以看到interfacetype定义中包含了接口类型typ
, 包路径名pkgpath
和用来存储接口方法集的切片mhdr
。
我们回到itab结构体定义,上面学习inter
字段存储的是接口本身的信息(接口类型、方法集等),那么剩下的_type
和fun
被分别用来存储具体类型信息和具体类型实现了接口方法的调用地址:
_type
是具体的具体类型信息fun
存储一组函数指针,是一个用于动态分发的虚函数表hash
字段是_type.hash
的缓存,当需要将接口类型转换成具体的类型时,使用该字段判断转换的目标类型是否和具体类型_type
一样
上面分析了iface struct的定义,总结如下:
- iface用来在运行时表示拥有方法的接口类型的变量
- iface内部有两个指针字段: tab和data。data"指向"当前被赋值给接口类型变量的具体类型变量值,tab存储了接口类型信息、接口方法信息、具体类型信息及具体类型信息实现接口方法的调用地址表
例1:
1package main
2
3import "fmt"
4
5type Flyable interface {
6 Fly()
7}
8
9type Bird struct {
10}
11
12func (b *Bird) Fly() {
13 fmt.Println("Bird fly.")
14}
15
16func main() {
17 var f Flyable
18 println(f, f == nil) // (0x0,0x0) true
19
20 var bird *Bird
21 f = bird
22 println(f, f == nil) // (0x10991e0,0x0) false
23
24 bird = &Bird{}
25 f = bird
26 println(f, f == nil) // (0x1099220,0xc00005ef70) false
27}
我们编写上面例1的代码,并使用delve调试工具在第18行println
函数出加断点调试,将代码执行到第18行断点处:
1dlv debug
2Type 'help' for list of commands.
3(dlv) b main.go:18
4Breakpoint 1 set at 0x107f87e for main.main() ./main.go:18
5(dlv) c
6> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x107f87e)
7 13: fmt.Println("Bird fly.")
8 14: }
9 15:
10 16: func main() {
11 17: var f Flyable
12=> 18: println(f, f == nil) // (0x0,0x0) true
13 19:
14 20: var bird *Bird
15 21: f = bird
16 22: println(f, f == nil) // (0x10991e0,0x0) false
17 23:
18(dlv)
使用disass
命令查看这段代码的反汇编:
1(dlv) disass
2TEXT main.main(SB) main.go
3 main.go:16 0x107f860 493b6610 cmp rsp, qword ptr [r14+0x10]
4 main.go:16 0x107f864 0f860f010000 jbe 0x107f979
5 main.go:16 0x107f86a 4883ec48 sub rsp, 0x48
6 main.go:16 0x107f86e 48896c2440 mov qword ptr [rsp+0x40], rbp
7 main.go:16 0x107f873 488d6c2440 lea rbp, ptr [rsp+0x40]
8 main.go:17 0x107f878 440f117c2430 movups xmmword ptr [rsp+0x30], xmm15
9=> main.go:18 0x107f87e* c644241701 mov byte ptr [rsp+0x17], 0x1
10 main.go:18 0x107f883 e8d822fbff call $runtime.printlock
11 main.go:18 0x107f888 488b442430 mov rax, qword ptr [rsp+0x30]
12 main.go:18 0x107f88d 488b5c2438 mov rbx, qword ptr [rsp+0x38]
13 main.go:18 0x107f892 e8092dfbff call $runtime.printiface
14 main.go:18 0x107f897 e8e424fbff call $runtime.printsp
15 main.go:18 0x107f89c 0fb6442417 movzx eax, byte ptr [rsp+0x17]
16 main.go:18 0x107f8a1 e85a25fbff call $runtime.printbool
17 main.go:18 0x107f8a6 e81525fbff call $runtime.printnl
18 main.go:18 0x107f8ab e83023fbff call $runtime.printunlock
19......
可以看到第18行会调用runtime.printiface
函数,在runtime.printiface上打个断点,并执行到该断点处:
1(dlv) b runtime.printiface
2Breakpoint 2 set at 0x10325a6 for runtime.printiface() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/print.go:260
3(dlv) c
4> runtime.printiface() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/print.go:260 (hits goroutine(1):1 total:1) (PC: 0x10325a6)
5Warning: debugging optimized function
6 255:
7 256: func printeface(e eface) {
8 257: print("(", e._type, ",", e.data, ")")
9 258: }
10 259:
11=> 260: func printiface(i iface) {
12 261: print("(", i.tab, ",", i.data, ")")
13 262: }
14 263:
15(dlv)
可以看到边以及将println(f)
替换成了runtime.printiface
。runtime.printiface的实现相当简单,就是但因了iface结构体中的tab和data字段。
学习了runtime.printiface函数之后,再看一下例1的代码:
- 第17行初始化了一个Flyable接口的变量f,注意这个接口的方法集不为空,因此它在运行时表示为iface。
- 第18行使用
println(f, f == nil)
打印f,此时f还未被赋值,打印结果为(0x0,0x0) true
。即f在运行时iface表示中的tab和data指针字段都是0x0(空的),因此f == nil
为true。 - 第18行说明了只有iface为
(0x0, 0x0)
时,它才能和nil
划等号。 - 第20行声明了一个Bird的结构体指针bird变量, bird的值为nil,bird的类型为*Bird
- 第21行将具体类型变量bird赋值给接口变量f时,f的运行时表示iface中的data将会被赋值为具体类型变量的值nil,因此data的值将会是0x0,而tab中会存储具体类型和接口类型的信息后就不再是0x0了。因此第22行
println(f, f == nil)
打印结果是(0x10991e0,0x0) false
。此时虽然iface中的data为空(0x0),但tab不再为空(0x10991e0),所以f == nil
为false。 - 第24行为具体类型指针变量bird分配了内存,bird不再为nil
- 第25行将bird赋值给f,此时f的运行时表示iface中的tab和data都不为空,因此第26行打印结果是
(0x1099220,0xc00005ef70) false
,f == nil
为false。
1.2 eface struct #
前面学习了iface后,再学eface就十分简单了。
eface的定义如下:
1// $GOROOT/src/runtime/runtime2.go
2type eface struct {
3 _type *_type
4 data unsafe.Pointer
5}
- eface用来在运行时表示方法集为空的接口类型变量,如
interface{}
类型。 - eface结构体有两个指针类型的字段
_type
和data
。_type
表示具体类型的类型信息,data执行具体类型变量的值。
例2:
1package main
2
3import "fmt"
4
5type Bird struct {
6}
7
8func (b *Bird) Fly() {
9 fmt.Println("Bird fly.")
10}
11
12func main() {
13 var f interface{}
14 println(f, f == nil) // (0x0,0x0) true
15
16 var bird *Bird
17 f = bird
18 println(f, f == nil) // (0x1074f40,0x0) false
19
20 bird = &Bird{}
21 f = bird
22 println(f, f == nil) // (0x1074f40,0xc000092f70) false
23}
例2中println(f)
println一个eface变量,将会被编译器替换成runtime.printeface
。
1// $GOROOT/src/runtime/print.go
2func printeface(e eface) {
3 print("(", e._type, ",", e.data, ")")
4}
- 第13行初始化了一个
interface{}
接口的变量f,注意这个接口的方法集为空,因此它在运行时表示为eface。 - 第14行使用
println(f, f == nil)
打印f,此时f还未被赋值,打印结果为(0x0,0x0) true
。即f在运行时eface表示中的_type
和data
指针字段都是0x0(空的),因此f == nil
为true。 - 第14行说明了只有eface为
(0x0, 0x0)
时,它才能和nil
划等号。 - 第16行声明了一个Bird的结构体指针bird变量, bird的值为nil,bird的类型为*Bird
- 第17行将具体类型变量bird赋值给接口变量f时,f的运行时表示eface中的data将会被赋值为具体类型变量的值nil,因此data的值将会是0x0,而_type中存储具体类型信息后就不再是0x0了。因此第18行
println(f, f == nil)
打印结果是(0x1074f40,0x0) false
。此时虽然eface中的data为空(0x0),但_type
不再为空(0x1074f40),所以f == nil
为false。 - 第20行为具体类型指针变量bird分配了内存,bird不再为nil
- 第21行将bird赋值给f,此时f的运行时表示eface中的
_type
和data都不为空,因此第22行打印结果是(0x1074f40,0xc000092f70) false
,f == nil
为false。
1.3 iface和eface示意图 #
下面根据前面1.1和1.2学习的内容,当将一个具体类型的变量赋给接口类型的变量时,整理绘制了如下的iface和eface在运行时表示的示意图。
当将一个具体类型的变量赋值给一个方法集不为空的接口类型的变量时,会创建一个iface结构体,iface结构体中的tab字段指向接口类型信息和具体类型信息,iface结构体的data字段"指向"具体类型的变量值。
当将一个具体类型的变量赋值给一个方法集为空的接口类型的变量时(例如interface{}
),会创建一个eface结构体,eface结构体的_type
字段是具体类型信息,eface结构体的data字段"指向"具体类型的变量值。
因此,将一个具体类型变量赋值给一个具体类型的变量的操作,会发生iface或eface的创建操作,这个操作可以理解为是一种装箱操作(Boxing),即将具体类型的data装箱为iface或eface。
2.理解Go interface的装箱操作 #
前面提到,当将具体类型变量的值赋值给接口类型的变量时,会进行iface或eface的装箱操作,iface或eface的data字段会"指向"具体类型的变量的值。 这里这个"指向"我们加了引号,需要好好理解它。
不管是iface还是eface,它们的data字段都是一个unsafe.Pointer
类型的指针,那这就面临以下几个问题:
- 如果具体类型的值是值类型的话,那么在装箱操作时,是直接将值的地址直接赋值给data这个指针吗?还是会拷贝一份具体类型的值,将拷贝的值的地址赋值为data这个指针?
- 如果具体类型的值是指针类型的话,那么在装箱操作时,是直接将这个指针值赋值给data吗? 还是会拷贝一份具体类型指向的值,将拷贝的值的地址赋值为data这个指针?
可以从下面例3和例4两个例子中找到答案。
例3:
1package main
2
3import "fmt"
4
5type Bird struct {
6 name string
7 color int
8}
9
10func (b Bird) Fly() {
11}
12
13type Flyable interface {
14 Fly()
15}
16
17func main() {
18 bird := Bird{name: "a", color: 100}
19 var efc interface{} = bird // eface boxing
20 var ifc Flyable = bird // iface boxing
21
22 fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird={name:a color:100}, efc={name:a color:100}, ifc={name:a color:100}
23 println(&bird, efc, ifc) // 0xc00005eee0 (0x109a160,0xc00000c030) (0x10c1620,0xc00000c048)
24 bird.name = "b"
25 fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird={name:b color:100}, efc={name:a color:100}, ifc={name:a color:100}
26 println(&bird, efc, ifc) // 0xc00005eee0 (0x109a160,0xc00000c030) (0x10c1620,0xc00000c048)
27}
例3中将结构体bird这个值类型进行装箱操作为eface和iface时,从打印结果可以看出, bird的地址是0xc00005eee0
,efc中data的值是0xc00000c030
, ifc中data的值是0xc00000c048
。
efc和ifc中的data没有指向具体类型的值,efc和ifc应该是各自都将具体类型的值拷贝了一份,然后指向了拷贝的值。为了验证这个问题,还是祭出dlv + disass
反汇编大法查看一下汇编代码,当然也可以使用go tool compile -S
,只是我用惯了前者。
1dlv debug
2Type 'help' for list of commands.
3(dlv) b main.go:19
4Breakpoint 1 set at 0x10ad885 for main.main() ./main.go:19
5(dlv) c
6> main.main() ./main.go:19 (hits goroutine(1):1 total:1) (PC: 0x10ad885)
7 14: Fly()
8 15: }
9 16:
10 17: func main() {
11 18: bird := Bird{name: "a", color: 100}
12=> 19: var efc interface{} = bird // eface boxing
13 20: var ifc Flyable = bird // iface boxing
14 21:
15 22: fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird={name:a color:100}, efc={name:a color:100}, ifc={name:a color:100}
16 23: println(&bird, efc, ifc) // 0xc00005eee0 (0x109a160,0xc00000c030) (0x10c1620,0xc00000c048)
17 24: bird.name = "b"
18
19
20(dlv) disass
21TEXT main.main(SB) main.go
22 main.go:17 0x10ad820 4c8da42450ffffff lea r12, ptr [rsp+0xffffff50]
23 main.go:17 0x10ad828 4d3b6610 cmp r12, qword ptr [r14+0x10]
24 main.go:17 0x10ad82c 0f8690040000 jbe 0x10adcc2
25 main.go:17 0x10ad832 4881ec30010000 sub rsp, 0x130
26 main.go:17 0x10ad839 4889ac2428010000 mov qword ptr [rsp+0x128], rbp
27 main.go:17 0x10ad841 488dac2428010000 lea rbp, ptr [rsp+0x128]
28 main.go:18 0x10ad849 440f11bc2498000000 movups xmmword ptr [rsp+0x98], xmm15
29 main.go:18 0x10ad852 48c78424a800000000000000 mov qword ptr [rsp+0xa8], 0x0
30 main.go:18 0x10ad85e 488d0d33830100 lea rcx, ptr [rip+0x18333]
31 main.go:18 0x10ad865 48898c2498000000 mov qword ptr [rsp+0x98], rcx
32 main.go:18 0x10ad86d 48c78424a000000001000000 mov qword ptr [rsp+0xa0], 0x1
33 main.go:18 0x10ad879 48c78424a800000064000000 mov qword ptr [rsp+0xa8], 0x64
34=> main.go:19 0x10ad885* 48898c24c8000000 mov qword ptr [rsp+0xc8], rcx
35 main.go:19 0x10ad88d 48c78424d000000001000000 mov qword ptr [rsp+0xd0], 0x1
36 main.go:19 0x10ad899 48c78424d800000064000000 mov qword ptr [rsp+0xd8], 0x64
37 main.go:19 0x10ad8a5 488d05d4ed0000 lea rax, ptr [rip+0xedd4]
38 main.go:19 0x10ad8ac 488d9c24c8000000 lea rbx, ptr [rsp+0xc8]
39 main.go:19 0x10ad8b4 e847c0f5ff call $runtime.convT2E
40 main.go:19 0x10ad8b9 4889442468 mov qword ptr [rsp+0x68], rax
41 main.go:19 0x10ad8be 48895c2470 mov qword ptr [rsp+0x70], rbx
42 main.go:20 0x10ad8c3 488b8c2498000000 mov rcx, qword ptr [rsp+0x98]
43 main.go:20 0x10ad8cb 488b9424a0000000 mov rdx, qword ptr [rsp+0xa0]
44 main.go:20 0x10ad8d3 488bb424a8000000 mov rsi, qword ptr [rsp+0xa8]
45 main.go:20 0x10ad8db 48898c24c8000000 mov qword ptr [rsp+0xc8], rcx
46 main.go:20 0x10ad8e3 48899424d0000000 mov qword ptr [rsp+0xd0], rdx
47 main.go:20 0x10ad8eb 4889b424d8000000 mov qword ptr [rsp+0xd8], rsi
48 main.go:20 0x10ad8f3 488d059e480200 lea rax, ptr [rip+0x2489e]
49 main.go:20 0x10ad8fa 488d9c24c8000000 lea rbx, ptr [rsp+0xc8]
50 main.go:20 0x10ad902 e8d9c2f5ff call $runtime.convT2I
51 main.go:20 0x10ad907 4889442458 mov qword ptr [rsp+0x58], rax
52 main.go:20 0x10ad90c 48895c2460 mov qword ptr [rsp+0x60], rbx
53 main.go:22 0x10ad911 488b8c2498000000 mov rcx, qword ptr [rsp+0x98]
例3代码的第19行和第20行的装箱操作,从汇编代码中找到了对应位置发现调用了runtime.convT2E
和runtime.convT2I
两个函数。进一步调试进去查看这两个函数的内容:
1(dlv) b runtime.convT2E
2Breakpoint 2 set at 0x1009906 for runtime.convT2E() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/iface.go:318
3(dlv) b runtime.convT2I
4Breakpoint 3 set at 0x1009be6 for runtime.convT2I() /usr/local/Cellar/go/1.17.3/libexec/src/runtime/iface.go:405
在$GOROOT/src/runtime/iface.go
中找到了这两个函数的实现:
1func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
2 if raceenabled {
3 raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
4 }
5 if msanenabled {
6 msanread(elem, t.size)
7 }
8 x := mallocgc(t.size, t, true)
9 // TODO: We allocate a zeroed object only to overwrite it with actual data.
10 // Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
11 typedmemmove(t, x, elem)
12 e._type = t
13 e.data = x
14 return
15}
16
17
18func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
19 t := tab._type
20 if raceenabled {
21 raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
22 }
23 if msanenabled {
24 msanread(elem, t.size)
25 }
26 x := mallocgc(t.size, t, true)
27 typedmemmove(t, x, elem)
28 i.tab = tab
29 i.data = x
30 return
31}
这两个函数中的x变量都是使用mallocgc重新分配的内存,然后拷贝了具体类型的值。
例4:
1package main
2
3import "fmt"
4
5type Bird struct {
6 name string
7 color int
8}
9
10func (b *Bird) Fly() {
11}
12
13type Flyable interface {
14 Fly()
15}
16
17func main() {
18 bird := &Bird{name: "a", color: 100}
19 var efc interface{} = bird // eface boxing
20 var ifc Flyable = bird // iface boxing
21
22 fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird=&{name:a color:100}, efc=&{name:a color:100}, ifc=&{name:a color:100}
23 println(bird, efc, ifc) // 0xc00000c030 (0x10954e0,0xc00000c030) (0x10c1420,0xc00000c030)
24 bird.name = "b"
25 fmt.Printf("bird=%+v, efc=%+v, ifc=%+v\n", bird, efc, ifc) // bird=&{name:b color:100}, efc=&{name:b color:100}, ifc=&{name:b color:100}
26 println(bird, efc, ifc) // 0xc00000c030 (0x10954e0,0xc00000c030) (0x10c1420,0xc00000c030)
27}
例4中将结构体指针bird这个指针类型进行装箱操作为eface和iface时,从打印结果可以看出, bird的地址, efc中data的值, ifc中data的值,三者都是0xc00000c030
。
efc和ifc中的data直接使用了被装箱的指针类型值。此时如果祭出dlv + disass
大法查看汇编代码的话,因为例4的装箱操作不再涉及值的拷贝,所以不会再调用runtime.convT2E和runtime.convT2I。
另外,需要注意在$GOROOT/src/runtime/iface.go
中还有很多convXXX
函数,这些都是Go为了不同类型值赋值给接口变量进行装箱操作的实现。例如包含convTslice
, convTstring
等,因为装箱是一个有性能损耗的操作,从这些函数的注释上可以看出Go本身不断在对这块进行优化。