理解Go interface的两种底层实现:iface和eface

理解Go interface的两种底层实现:iface和eface

2018-11-06
Go

Go语言interface的运行时实现的源码位于$GOROOT/src/runtime/runtime2.go中。 在Go的不同版本中,interface的实现可能会有不同,但整体结构变化不大,本文基于Go 1.17。

1.两类接口的运行时实现 #

可以在runtime/runtime2.go中找到Go的接口类型变量在运行时的表示,如下是ifaceeface两个结构体:

 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,它有两个指针字段tabdata

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字段存储的是接口本身的信息(接口类型、方法集等),那么剩下的_typefun被分别用来存储具体类型信息和具体类型实现了接口方法的调用地址:

  • _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) falsef == 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结构体有两个指针类型的字段_typedata_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表示中的_typedata指针字段都是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) falsef == nil为false。

1.3 iface和eface示意图 #

下面根据前面1.1和1.2学习的内容,当将一个具体类型的变量赋给接口类型的变量时,整理绘制了如下的iface和eface在运行时表示的示意图。

go-interface-iface.png

go-interface-eface.png

当将一个具体类型的变量赋值给一个方法集不为空的接口类型的变量时,会创建一个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.convT2Eruntime.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本身不断在对这块进行优化。

参考 #

© 2024 青蛙小白