0%

《Golang》Interface原理

Interface原理

ifaceeface两个类型

Golang的Interface中主要有ifaceeface两个类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type iface struct {  // 非空interface类型(var itest Itest)实际上是这个类型,既包含接口的类型,也包含持有值的类型
tab *itab // 描述了接口类型信息、持有值的类型信息、接口中定义的方法。如var itest Itest = &Test{},itest会被翻译成iface struct
data unsafe.Pointer // 指向持有值数据的指针
}

type eface struct { // 空interface类型(var itest interface{})实际上是这个struct,只包含持有值的类型
_type *_type // 表示持有值的类型信息。如var bb1 interface{} = &Test{},bb1会被翻译成eface struct,_type表示Test的类型信息
data unsafe.Pointer // 指向持有值数据的指针
}

type itab struct {
inter *interfacetype // 描述了 interface 本身的类型
_type *_type // 描述了 interface 所持有的值的类型
hash uint32 // copy of _type.hash. Used for type switches. _type.hash用于类型转换
_ [4]byte // 缓存行填充,无具体含义
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. 接口中定义的方法集合,这个字段的长度是可变的(编译器处理),第n个元素指向第n个方法
}

type _type struct {
size uintptr // 源数据占用字节的大小。如果是指针类型,这里就是8字节
ptrdata uintptr // 表示源数据使用到的所有指针占用的空间大小(不是占用大小累加,而是最大指针-最小指针)。var interface{} a = &Test{},源数据是&Test{},不管Test类型中是否有指针,这里都是8
hash uint32 // 类型的hash
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}

类型的含义请看上面的代码注释

可以看出iface其实是包含了eface的,那么为什么空interface和非空interface不全都使用iface呢?

我想原因可能是节约内存,毕竟ifaceeface结构体要大一些,而且空interface一般用的很多

下面看一个例子加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package main

import (
"fmt"
"runtime"
"unsafe"
)

type tflag uint8
type nameOff int32
type typeOff int32

type imethod struct {
name nameOff
ityp typeOff
}

type name struct {
bytes *byte
}

type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}

type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}

type itab struct {
inter *interfacetype // 描述了 interface 本身的类型
_type *_type // 描述了 interface 所持有的值的类型
hash uint32 // copy of _type.hash. Used for type switches. _type.hash用于类型转换
_ [4]byte // 缓存行填充,无具体含义
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type eface struct { // 空interface类型实际上是这个struct
_type *_type
data unsafe.Pointer
}

type iface struct { // 非空interface类型实际上是这个类型
tab *itab
data unsafe.Pointer
}

type Itest interface {
Test()
Test1()
}

type Test struct {
a *Test
b int64
c *Test
}

func (test *Test) Test() {
fmt.Println(11)
}

func (test *Test) Test1() {
fmt.Println(22)
}

func main() {
var bb interface{} = Test{} // bb 是空interface。编译器会为bb填充_type中所有数据
eface1 := *(*eface)(unsafe.Pointer(&bb))
fmt.Printf("eface1._type.size = %#x\n", eface1._type.size)
fmt.Printf("eface1._type.kind = %#x\n", eface1._type.kind)
fmt.Printf("eface1._type.hash = %#x\n", eface1._type.hash)
fmt.Printf("eface1._type.ptrdata = %#x\n", eface1._type.ptrdata)

var itest Itest = &Test{} // itest是非空interface。编译器会为itest填充itab中所有数据
iface1 := *(*iface)(unsafe.Pointer(&itest))
fmt.Printf("iface1.tab._type.size = %#x\n", iface1.tab._type.size)
fmt.Printf("iface1.tab._type.kind = %#x\n", iface1.tab._type.kind)
fmt.Printf("iface1.tab._type.hash = %#x\n", iface1.tab._type.hash)
fmt.Printf("iface1.tab._type.ptrdata = %#x\n", iface1.tab._type.ptrdata)

fmt.Printf("itab.fun数组的栈指针 = %#p\n", &iface1.tab.fun)
fmt.Printf("itab.fun数组的栈指针 = %#p\n", &iface1.tab.fun[0]) // 栈指针
fmt.Printf("itab.fun数组第一个元素的值 = %#x\n", iface1.tab.fun[0]) // 数组第一个元素的uintptr类型值
testMethod1 := *(*uintptr)(unsafe.Pointer(&iface1.tab.fun)) // 数组第一个元素的uintptr类型值
fmt.Printf("itab.fun数组第一个元素的值 = %#x\n", testMethod1)
test1Method1 := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&iface1.tab.fun)) + 8)) // 数组第二个元素的uintptr类型值
fmt.Printf("itab.fun数组第二个元素的值 = %#x\n", test1Method1)
testMethod := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&(*iface1.tab))) + 24)) // 数组第一个元素的uintptr类型值
fmt.Printf("itab.fun数组第一个元素的值 = %#x\n", testMethod)
test1Method := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&(*iface1.tab))) + 32)) // 数组第二个元素的uintptr类型值
fmt.Printf("itab.fun数组第二个元素的值 = %#x\n", test1Method)
fmt.Println(runtime.FuncForPC(testMethod).Name())
fmt.Println(runtime.FuncForPC(test1Method).Name())

itest.Test() // 会找到itab.fun数组第一个元素的uintptr类型值,然后call
itest.Test1() // 会找到itab.fun数组第二个元素的uintptr类型值,然后call
}


// Output:
// eface1._type.size = 0x18
// eface1._type.kind = 0x19
// eface1._type.hash = 0x590dfa97
// eface1._type.ptrdata = 0x18
// iface1.tab._type.size = 0x8
// iface1.tab._type.kind = 0x36
// iface1.tab._type.hash = 0xdb6accc6
// iface1.tab._type.ptrdata = 0x8
// itab.fun数组的栈指针 = 10ece98
// itab.fun数组的栈指针 = 10ece98
// itab.fun数组第一个元素的值 = 0x109ea50
// itab.fun数组第一个元素的值 = 0x109ea50
// itab.fun数组第二个元素的值 = 0x109eae0
// itab.fun数组第一个元素的值 = 0x109ea50
// itab.fun数组第二个元素的值 = 0x109eae0
// main.(*Test).Test
// main.(*Test).Test1
// 11
// 22

接口实现与断言

看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
)

type Itest interface {
Test()
Test1()
}

type Test struct {
a *Test
b int64
c *Test
}

func (test *Test) Test() {
fmt.Println(11)
}

func (test *Test) Test1() {
fmt.Println(22)
}

func test(a Itest) {
aa, ok := a.(*Test)
fmt.Println(ok)
aa.Test()
}

func main() {
a := &Test{}
test(a)
}

下面翻译成汇编语言看看

首先看main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
test.go:32            0x109d1b0               65488b0c2530000000      MOVQ GS:0x30, CX                                
test.go:32 0x109d1b9 483b6110 CMPQ 0x10(CX), SP
test.go:32 0x109d1bd 766f JBE 0x109d22e // 这里往前都是栈检查
test.go:32 0x109d1bf 4883ec48 SUBQ $0x48, SP // 分配栈帧
test.go:32 0x109d1c3 48896c2440 MOVQ BP, 0x40(SP) // 保存栈基
test.go:32 0x109d1c8 488d6c2440 LEAQ 0x40(SP), BP // 设置栈基
test.go:33 0x109d1cd 48c744242800000000 MOVQ $0x0, 0x28(SP) // 给一个0到0x28(SP)处
test.go:33 0x109d1d6 0f57c0 XORPS X0, X0
test.go:33 0x109d1d9 0f11442430 MOVUPS X0, 0x30(SP)
test.go:33 0x109d1de 488d442428 LEAQ 0x28(SP), AX // 将0x28(SP)处的栈指针放入AX中
test.go:33 0x109d1e3 4889442418 MOVQ AX, 0x18(SP) // 0x18(SP)就是a变量的值
test.go:33 0x109d1e8 8400 TESTB AL, 0(AX)
test.go:33 0x109d1ea 48c744243000000000 MOVQ $0x0, 0x30(SP) // Test第二个字段,默认值0
test.go:33 0x109d1f3 48c744242800000000 MOVQ $0x0, 0x28(SP) // Test第一个字段,默认值0
test.go:33 0x109d1fc 48c744243800000000 MOVQ $0x0, 0x38(SP) // Test第三个字段,默认值0
test.go:33 0x109d205 4889442410 MOVQ AX, 0x10(SP)
test.go:34 0x109d20a 4889442420 MOVQ AX, 0x20(SP)
test.go:34 0x109d20f 488d0d8ad20400 LEAQ go.itab.*main.Test,main.Itest(SB), CX // Test的_type数据地址放入CX
test.go:34 0x109d216 48890c24 MOVQ CX, 0(SP) // eface第一个字段_type入栈
test.go:34 0x109d21a 4889442408 MOVQ AX, 0x8(SP) // eface第二个字段data入栈,data就是a的值,也就是Test实例的栈指针
test.go:34 0x109d21f e87cfeffff CALL main.test(SB) // 调用test函数。上面两个入栈的就是转换后的eface实例
test.go:35 0x109d224 488b6c2440 MOVQ 0x40(SP), BP
test.go:35 0x109d229 4883c448 ADDQ $0x48, SP
test.go:35 0x109d22d c3 RET
test.go:32 0x109d22e e89dc1fbff CALL runtime.morestack_noctxt(SB)
test.go:32 0x109d233 e978ffffff JMP main.main(SB)

下面再看看test函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
test.go:24            0x109d0a0               65488b0c2530000000      MOVQ GS:0x30, CX                                
test.go:24 0x109d0a9 483b6110 CMPQ 0x10(CX), SP
test.go:24 0x109d0ad 0f86ef000000 JBE 0x109d1a2 // 这里往前都是栈检查
test.go:24 0x109d0b3 4883c480 ADDQ $-0x80, SP // 分配栈帧
test.go:24 0x109d0b7 48896c2478 MOVQ BP, 0x78(SP) // 保存栈基
test.go:24 0x109d0bc 488d6c2478 LEAQ 0x78(SP), BP // 设置栈基
test.go:25 0x109d0c1 48c744244800000000 MOVQ $0x0, 0x48(SP)
test.go:25 0x109d0ca 488b842488000000 MOVQ 0x88(SP), AX // eface的_type字段放入AX
test.go:25 0x109d0d2 488b8c2490000000 MOVQ 0x90(SP), CX // eface的data字段放入CX
test.go:25 0x109d0da 488d153fd30400 LEAQ go.itab.*main.Test,main.Itest(SB), DX // Test的_type数据地址放入DX
test.go:25 0x109d0e1 4839d0 CMPQ DX, AX // 比较DX和AX,如果相等,则表示可以转换,断言成功,否则断言失败
test.go:25 0x109d0e4 7405 JE 0x109d0eb // 相等则跳转
test.go:25 0x109d0e6 e9ae000000 JMP 0x109d199
test.go:25 0x109d0eb b801000000 MOVL $0x1, AX // 设置断言结果
test.go:25 0x109d0f0 eb00 JMP 0x109d0f2
test.go:25 0x109d0f2 48894c2448 MOVQ CX, 0x48(SP) // data字段放到0x48(SP)处
test.go:25 0x109d0f7 88442437 MOVB AL, 0x37(SP) // 断言结果放到0x37(SP)处
test.go:25 0x109d0fb 488b442448 MOVQ 0x48(SP), AX // data内容取出来
test.go:25 0x109d100 4889442438 MOVQ AX, 0x38(SP) // data字段放到0x38(SP)处
test.go:25 0x109d105 0fb6442437 MOVZX 0x37(SP), AX // 断言结果拿出来
test.go:25 0x109d10a 88442436 MOVB AL, 0x36(SP) // 断言结果ok放到0x36(SP)处
test.go:26 0x109d10e 0fb6442436 MOVZX 0x36(SP), AX // 这里往下都是fmt.Println打印
test.go:26 0x109d113 88442437 MOVB AL, 0x37(SP)
test.go:26 0x109d117 0f57c0 XORPS X0, X0
test.go:26 0x109d11a 0f11442450 MOVUPS X0, 0x50(SP)
test.go:26 0x109d11f 488d442450 LEAQ 0x50(SP), AX
test.go:26 0x109d124 4889442440 MOVQ AX, 0x40(SP)
test.go:26 0x109d129 8400 TESTB AL, 0(AX)
test.go:26 0x109d12b 0fb64c2437 MOVZX 0x37(SP), CX
test.go:26 0x109d130 488d1549d50000 LEAQ type.*+54368(SB), DX
test.go:26 0x109d137 4889542450 MOVQ DX, 0x50(SP)
test.go:26 0x109d13c 488d155dce0c00 LEAQ runtime.staticbytes(SB), DX
test.go:26 0x109d143 4801d1 ADDQ DX, CX
test.go:26 0x109d146 48894c2458 MOVQ CX, 0x58(SP)
test.go:26 0x109d14b 8400 TESTB AL, 0(AX)
test.go:26 0x109d14d eb00 JMP 0x109d14f
test.go:26 0x109d14f 4889442460 MOVQ AX, 0x60(SP)
test.go:26 0x109d154 48c744246801000000 MOVQ $0x1, 0x68(SP)
test.go:26 0x109d15d 48c744247001000000 MOVQ $0x1, 0x70(SP)
test.go:26 0x109d166 48890424 MOVQ AX, 0(SP)
test.go:26 0x109d16a 48c744240801000000 MOVQ $0x1, 0x8(SP)
test.go:26 0x109d173 48c744241001000000 MOVQ $0x1, 0x10(SP)
test.go:26 0x109d17c e83f98ffff CALL fmt.Println(SB) // 这里往上都是fmt.Println打印
test.go:27 0x109d181 488b442438 MOVQ 0x38(SP), AX // 0x38(SP)处的值放入AX,即data字段放入AX,也就是aa放入AX
test.go:27 0x109d186 48890424 MOVQ AX, 0(SP) // AX作为参数
test.go:27 0x109d18a e8d1fdffff CALL main.(*Test).Test(SB) // 调用Test,也就是aa.Test()
test.go:28 0x109d18f 488b6c2478 MOVQ 0x78(SP), BP
test.go:28 0x109d194 4883ec80 SUBQ $-0x80, SP
test.go:28 0x109d198 c3 RET
test.go:28 0x109d199 31c9 XORL CX, CX // 通过异或将CX置为0
test.go:28 0x109d19b 31c0 XORL AX, AX // 通过异或将AX置为0,设置了断言结果
test.go:25 0x109d19d e950ffffff JMP 0x109d0f2
test.go:24 0x109d1a2 e829c2fbff CALL runtime.morestack_noctxt(SB)
test.go:24 0x109d1a7 e9f4feffff JMP main.test(SB)
:-1 0x109d1ac cc INT $0x3
:-1 0x109d1ad cc INT $0x3
:-1 0x109d1ae cc INT $0x3
:-1 0x109d1af cc INT $0x3

可以看出,判断断言是否成功的方式是比较空接口中的_type字段的指针值是否等于目标类型的type信息的指针值

因为每个类型在编译器就被确定,他们的类型信息,也就是_type字段的内容都会被编译成常量放入了data区,同一个类型的类型信息将被放在data区同一个地方,所以可以通过比较指针值来判断能否断言

接口向接口的断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

type Test interface {
GetB() string
}

type Test1 interface {
Test
GetA() string
}

type Test2 struct {
Test
}

func main() {
var a Test = Test2{}
_ = a.(Test1)
}

结果是编译成功,但是运行失败

下面分析下为什么

反编译出来发现 _ = a.(Test1) 这一行编译成了下面的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
main.go:18            0x1056ea3               0f57c0                  XORPS X0, X0                                    
main.go:18 0x1056ea6 0f11442448 MOVUPS X0, 0x48(SP)
main.go:18 0x1056eab 488b442428 MOVQ 0x28(SP), AX
main.go:18 0x1056eb0 488b4c2430 MOVQ 0x30(SP), CX
main.go:18 0x1056eb5 488d15c4da0000 LEAQ runtime.types+55648(SB), DX
main.go:18 0x1056ebc 48891424 MOVQ DX, 0(SP) // iface.data
main.go:18 0x1056ec0 4889442408 MOVQ AX, 0x8(SP) // iface.tab
main.go:18 0x1056ec5 48894c2410 MOVQ CX, 0x10(SP) // 第一个参数inter,表示目标接口类型
main.go:18 0x1056eca e8e10efbff CALL runtime.assertI2I(SB)
main.go:18 0x1056ecf 488b442420 MOVQ 0x20(SP), AX
main.go:18 0x1056ed4 488b4c2418 MOVQ 0x18(SP), CX
main.go:18 0x1056ed9 48894c2448 MOVQ CX, 0x48(SP)
main.go:18 0x1056ede 4889442450 MOVQ AX, 0x50(SP)

可以看到接口向接口的断言会被编译成runtime.assertI2I方法的调用,检查放在了运行时,所以编译不会报错

runtime.assertI2I会检查源接口中持有的值是否完整实现了目标接口中的所有函数,如果缺少实现,则运行报错(这里会查找itabTable,itabTable中保存了整个进程所有的itab,查到了itab则用它,没查到则新增一个,新增时会检查持有值是否完全实现了接口类型,实现了则插入itabTable,断言成功并返回iface)

上面示例中,a接口中持有的值Test2{}并没有实现Test1接口中定义的GetA() string方法,所以断言失败

但如果加上下面一段

1
2
3
func (a Test2) GetA() string {
return ""
}

则可以断言成功,所以接口向接口的断言并不是看哪个接口包含哪个接口,而是看持有的值是否完全实现了目标接口定义的方法

unsafe.Pointer复习使用

上面例子中,从itab.fun数组中寻找第二个方法时,用到了unsafe.Pointer(运行时中大量使用,最好能掌握),可以参考文章《Golang》Unsafe包的用处

这里也顺带复习一下unsafe.Pointer的使用

看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
"unsafe"
)

type Test struct {
a *Test
b int64
c *Test
}

func (test *Test) Test() {
fmt.Println(11)
}

func (test *Test) Test1() {
fmt.Println(22)
}

func main() {
aa := Test{b: 1}
a := Test{a: &aa, b: 32}
fmt.Printf("a的栈指针:%p\n", &a)
fmt.Printf("a的栈指针:%p\n", &a.a)
fmt.Printf("a.a的内容:%p\n", a.a)
fmt.Printf("a.a的内容:%#v\n", *(*uintptr)(unsafe.Pointer(&a)))
fmt.Printf("a.a的内容:%#v\n", *(*uintptr)(unsafe.Pointer(&a.a)))
fmt.Printf("aa的栈指针:%p\n", &aa) // a.a的内容就是aa的栈指针
fmt.Printf("aa的栈指针:%p\n", &(*a.a)) // 注意跟&a.a区分
fmt.Printf("aa.b的栈指针:%p\n", &aa.b)
fmt.Printf("aa.b的栈指针:%#x\n", unsafe.Pointer(uintptr(unsafe.Pointer(&aa)) + 8))
fmt.Printf("aa.b的栈指针:%#x\n", unsafe.Pointer(uintptr(unsafe.Pointer(&(*a.a))) + 8))
fmt.Printf("aa.b的内容:%#x\n", aa.b)
fmt.Printf("aa.b的内容:%#x\n", a.a.b)
fmt.Printf("aa.b的内容:%#x\n", (*a.a).b)
fmt.Printf("aa.b的内容:%#x\n", *(*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(&aa)) + 8)))
fmt.Printf("aa.b的栈指针:%p\n", &a.a.b)
fmt.Printf("aa.b的栈指针:%p\n", &a.a.b)
}

// Output:
//a的栈指针:0xc00011a020
//a的栈指针:0xc00011a020
//a.a的内容:0xc00011a000
//a.a的内容:0xc00011a000
//a.a的内容:0xc00011a000
//aa的栈指针:0xc00011a000
//aa的栈指针:0xc00011a000
//aa.b的栈指针:0xc00011a008
//aa.b的栈指针:0xc00011a008
//aa.b的栈指针:0xc00011a008
//aa.b的内容:0x1
//aa.b的内容:0x1
//aa.b的内容:0x1
//aa.b的内容:0x1
//aa.b的栈指针:0xc00011a008
//aa.b的栈指针:0xc00011a008

上面要特别注意:

a.a是一个指向Test的指针,获取b的值严格的写法应该是(*a.a).b,但是这样写很麻烦,所以编译器允许a.a.b这种写法

总结

  1. iface是非空interface的底层类型,eface是空interface的底层类型。iface struct中的信息涵盖了eface,他们struct中都有持有值的类型,eface是空接口,所以没有自身的类型,而iface有自身的类型信息
  2. struct第一个字段的地址就是struct的地址
  3. 在空接口向具体类型的断言中,能否断言成功是通过比较空接口中的_type字段的指针值是否等于目标类型的type信息的指针值
  4. 在一个接口A向另一个接口B断言时,判断的是A持有的数据是否完全实现了B接口定义的方法,跟接口的包含关系无关



微信关注我,及时接收最新技术文章