golang(09) golang 接口內部實現


原文鏈接 http://www.limerence2017.com/2019/09/24/golang14/#more

前文介紹過golang interface用法,本文詳細剖析interface內部實現和細節。

empty interface實現細節

interface底層使用兩種類型實現的,一個是eface,一個是iface。當interface中沒有方法的時候,底層是通過eface實現的。
當interface包含方法時,那么它的底層是通過iface實現的。
對於iface和eface具體實現在go源碼runtime2.go中,我們看下源碼

1
2
3
4
type eface struct {
_type *_type
data unsafe.Pointer
}

 

可以看到eface包含兩個結構,一個是_type類型指針,一個是unsafe包的Pointer變量
繼續追蹤Pointer

1
2
type Pointer *ArbitraryType
type ArbitraryType int

 

可以看出Pointer實際上是int類型的指針。我們再看看_type類型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// 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
}

 

size 為該類型所占用的字節數量。
kind 表示類型的種類,如 bool、int、float、string、struct、interface 等。
str 表示類型的名字信息,它是一個 nameOff(int32) 類型,通過這個 nameOff,可以找到類型的名字字符串

eface結構總結圖
1.jpg
eface 分兩個部分, *_type 類型為實際類型轉化為type類型的指針,data為實際數據。

具體類型如何轉化為eface

我們寫一段程序efacedemo.go,然后用gobuild命令生成可執行文件,再用匯編查看下源碼。

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

import "fmt"

type EmpInter interface {
}

type EmpStruct struct {
num int
}

func main() {

emps := EmpStruct{num: 1}
var empi EmpInter
empi = emps
fmt.Println(empi)
fmt.Println(emps)

}

 

先用gcflags標記編譯生成可執行文件efacedemo
go build -gcflags “-l” -o efacedemo efacedemo.go
然后執行go tool objdump 將 可執行程序efacedemo中main包的main函數轉為匯編代碼
go tool objdump -s “main.main” efacedemo

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
efacedemo.go:12 0x48ffa0 65488b0c2528000000 MOVQ GS:0x28, CX
efacedemo.go:12 0x48ffa9 488b8900000000 MOVQ 0(CX), CX
efacedemo.go:12 0x48ffb0 483b6110 CMPQ 0x10(CX), SP
efacedemo.go:12 0x48ffb4 0f86ae000000 JBE 0x490068
efacedemo.go:12 0x48ffba 4883ec48 SUBQ $0x48, SP
efacedemo.go:12 0x48ffbe 48896c2440 MOVQ BP, 0x40(SP)
efacedemo.go:12 0x48ffc3 488d6c2440 LEAQ 0x40(SP), BP
efacedemo.go:16 0x48ffc8 48c7042401000000 MOVQ $0x1, 0(SP)
efacedemo.go:16 0x48ffd0 e8fb89f7ff CALL runtime.convT64(SB) //注意這里調用runtime包的convT64函數
efacedemo.go:16 0x48ffd5 488b442408 MOVQ 0x8(SP), AX
efacedemo.go:17 0x48ffda 0f57c0 XORPS X0, X0
efacedemo.go:17 0x48ffdd 0f11442430 MOVUPS X0, 0x30(SP)
efacedemo.go:17 0x48ffe2 488d0d17c20100 LEAQ runtime.types+111104(SB
efacedemo.go:17 0x48ffe9 48894c2430 MOVQ CX, 0x30(SP)
efacedemo.go:17 0x48ffee 4889442438 MOVQ AX, 0x38(SP)
efacedemo.go:17 0x48fff3 488d442430 LEAQ 0x30(SP), AX
efacedemo.go:17 0x48fff8 48890424 MOVQ AX, 0(SP)
efacedemo.go:17 0x48fffc 48c744240801000000 MOVQ $0x1, 0x8(SP)
efacedemo.go:17 0x490005 48c744241001000000 MOVQ $0x1, 0x10(SP)
efacedemo.go:17 0x49000e e8fd98ffff CALL fmt.Println(SB)
efacedemo.go:18 0x490013 48c7042401000000 MOVQ $0x1, 0(SP)
efacedemo.go:18 0x49001b e8b089f7ff CALL runtime.convT64(SB) //注意這里調用runtime包的convT64函數
efacedemo.go:18 0x490020 488b442408 MOVQ 0x8(SP), AX
efacedemo.go:18 0x490025 0f57c0 XORPS X0, X0
efacedemo.go:18 0x490028 0f11442430 MOVUPS X0, 0x30(SP)
efacedemo.go:18 0x49002d 488d0dccc10100 LEAQ runtime.types+111104(SB
efacedemo.go:18 0x490034 48894c2430 MOVQ CX, 0x30(SP)
efacedemo.go:18 0x490039 4889442438 MOVQ AX, 0x38(SP)
efacedemo.go:18 0x49003e 488d442430 LEAQ 0x30(SP), AX
efacedemo.go:18 0x490043 48890424 MOVQ AX, 0(SP)
efacedemo.go:18 0x490047 48c744240801000000 MOVQ $0x1, 0x8(SP)
efacedemo.go:18 0x490050 48c744241001000000 MOVQ $0x1, 0x10(SP)
efacedemo.go:18 0x490059 e8b298ffff CALL fmt.Println(SB)
efacedemo.go:20 0x49005e 488b6c2440 MOVQ 0x40(SP), BP
efacedemo.go:20 0x490063 4883c448 ADDQ $0x48, SP
efacedemo.go:20 0x490067 c3 RET
efacedemo.go:12 0x490068 e883f2fbff CALL runtime.morestack_noctx
efacedemo.go:12 0x49006d e92effffff JMP main.main(SB)

 

拋開寄存器尋址和寄存數據不談,我們看到efacedemo.go:16行, CALL runtime.convT64(SB)語句說明調用了runtime包的convT64函數
這個函數在runtime.go 中有聲明

1
2
3
4
5
6
7
// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer // val must be a slice

 

看注釋就知道是type類型轉化為interface類型,計算機將EmpStruct強制轉化為type類型后,type類型進一步轉化為interface,並且將EmpStruct數據
轉化為unsafe.Pointer,畢竟int在64位機器中為8字節,所以采用了convT64函數。
還有一個函數convT2E,這個函數是將type轉化為eface類型,大家可以讀一讀runtime源碼。
convT2E源碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
// TODO: We allocate a zeroed object only to overwrite it with actual data.
// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}

 

內部調用了typedmemove做類型判斷,所以一個類能否轉化為某個接口是在runtime這一層做判斷的。判斷條件就是我之前所說的是否
實現了該接口所有的方法。
將上面的代碼用eface圖解就是
2.jgp

具體類型如何轉換為iface

當接口中帶有方法的時候,接口底層的實現就是通過iface結構實現的。我們下一個帶方法的接口,然后反匯編一下。

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
package main

import "fmt"

type EmpInter interface {
GetNum() int
}

type EmpStruct struct {
num int
}

func (es *EmpStruct) GetNum() int {
return es.num
}

func main() {

emps := EmpStruct{num: 1}
var empi EmpInter
empi = &emps
fmt.Println(empi)
fmt.Println(emps)

}

 

和之前操作一樣,先編譯
go build -gcflags “-l” -o ifacedemo ifacedemo.go
然后反匯編找到指令
go tool objdump -s “main.main” ifacedemo
生成的匯編指令如下

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
ifacedemo.go:17 0x48ffb0 65488b0c2528000000 MOVQ GS:0x28, CX
ifacedemo.go:17 0x48ffb9 488b8900000000 MOVQ 0(CX), CX
ifacedemo.go:17 0x48ffc0 483b6110 CMPQ 0x10(CX), SP
ifacedemo.go:17 0x48ffc4 0f86c1000000 JBE 0x49008b
ifacedemo.go:17 0x48ffca 4883ec50 SUBQ $0x50, SP
ifacedemo.go:17 0x48ffce 48896c2448 MOVQ BP, 0x48(SP)
ifacedemo.go:17 0x48ffd3 488d6c2448 LEAQ 0x48(SP), BP
ifacedemo.go:19 0x48ffd8 488d0521c30100 LEAQ runtime.types+111360(SB), AX
ifacedemo.go:19 0x48ffdf 48890424 MOVQ AX, 0(SP)
ifacedemo.go:19 0x48ffe3 e868b3f7ff CALL runtime.newobject(SB)
ifacedemo.go:19 0x48ffe8 488b442408 MOVQ 0x8(SP), AX
ifacedemo.go:19 0x48ffed 4889442430 MOVQ AX, 0x30(SP)
ifacedemo.go:19 0x48fff2 48c70001000000 MOVQ $0x1, 0(AX)
ifacedemo.go:22 0x48fff9 488b0d48ce0400 MOVQ go.itab.*main.EmpStruct,main.EmpInter+8(SB), CX
ifacedemo.go:22 0x490000 0f57c0 XORPS X0, X0
ifacedemo.go:22 0x490003 0f11442438 MOVUPS X0, 0x38(SP)
ifacedemo.go:22 0x490008 48894c2438 MOVQ CX, 0x38(SP)
ifacedemo.go:22 0x49000d 4889442440 MOVQ AX, 0x40(SP)
ifacedemo.go:22 0x490012 488d4c2438 LEAQ 0x38(SP), CX
ifacedemo.go:22 0x490017 48890c24 MOVQ CX, 0(SP)
ifacedemo.go:22 0x49001b 48c744240801000000 MOVQ $0x1, 0x8(SP)
ifacedemo.go:22 0x490024 48c744241001000000 MOVQ $0x1, 0x10(SP)
ifacedemo.go:22 0x49002d e8de98ffff CALL fmt.Println(SB)
ifacedemo.go:23 0x490032 488b442430 MOVQ 0x30(SP), AX
ifacedemo.go:23 0x490037 488b00 MOVQ 0(AX), AX
ifacedemo.go:23 0x49003a 48890424 MOVQ AX, 0(SP)
ifacedemo.go:23 0x49003e e88d89f7ff CALL runtime.convT64(SB)
ifacedemo.go:23 0x490043 488b442408 MOVQ 0x8(SP), AX
ifacedemo.go:23 0x490048 0f57c0 XORPS X0, X0
ifacedemo.go:23 0x49004b 0f11442438 MOVUPS X0, 0x38(SP)
ifacedemo.go:23 0x490050 488d0da9c20100 LEAQ runtime.types+111360(SB), CX
ifacedemo.go:23 0x490057 48894c2438 MOVQ CX, 0x38(SP)
ifacedemo.go:23 0x49005c 4889442440 MOVQ AX, 0x40(SP)
ifacedemo.go:23 0x490061 488d442438 LEAQ 0x38(SP), AX
ifacedemo.go:23 0x490066 48890424 MOVQ AX, 0(SP)
ifacedemo.go:23 0x49006a 48c744240801000000 MOVQ $0x1, 0x8(SP)
ifacedemo.go:23 0x490073 48c744241001000000 MOVQ $0x1, 0x10(SP)
ifacedemo.go:23 0x49007c e88f98ffff CALL fmt.Println(SB)
ifacedemo.go:25 0x490081 488b6c2448 MOVQ 0x48(SP), BP
ifacedemo.go:25 0x490086 4883c450 ADDQ $0x50, SP
ifacedemo.go:25 0x49008a c3 RET
ifacedemo.go:17 0x49008b e860f2fbff CALL runtime.morestack_noctxt(SB)
ifacedemo.go:17 0x490090 e91bffffff JMP main.main(SB)
:-1 0x490095 cc INT $0x3
:-1 0x490096 cc INT $0x3
:-1 0x490097 cc INT $0x3
:-1 0x490098 cc INT $0x3
:-1 0x490099 cc INT $0x3
:-1 0x49009a cc INT $0x3
:-1 0x49009b cc INT $0x3
:-1 0x49009c cc INT $0x3
:-1 0x49009d cc INT $0x3
:-1 0x49009e cc INT $0x3
:-1 0x49009f cc INT $0x3

 

拋開寄存器尋址和移動數據,我們查看call和重要的數據copy
19行LEAQ runtime.types+111360(SB), AX做了類型上的轉換
19行CALL runtime.newobject(SB)調用了runtime包的newobject方法,開辟我們結構體的指針
22行MOVQ go.itab.*main.EmpStruct,main.EmpInter+8(SB), CX將我們結構體部分信息保存在itab中。
23行 CALL runtime.convT64(SB), 實際上將64位的num轉化為數據存在data域中。
這些函數都可以在runtime包中找到,讀者可以自己閱讀。另外,還有個重要函數convT2I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}

 

這個函數將Type類型轉化為Iface類型。類似的函數還有convT2Inoptr(Type轉Iface指針)。讀者可以自己閱讀源碼。
通過上面的調試和分析,我們知道iface中結構和eface略有不同,多出一個itab類型的結構,我們看看iface源碼

1
2
3
4
type iface struct {
tab *itab
data unsafe.Pointer
}

 

和eface不同,iface的第一個字段是itab指針。我們繼續查看itab的定義

1
2
3
4
5
6
7
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

 

itab 各參數含義
inter和_type共同確認實際的類型信息,因為在接口中有方法的時候,itab要保存接口方法的一些額外信息,如名字,類型等。
hash 用於查詢類型和判斷類型,比如接口賦值,接口轉換判斷等。
fun 為具體的方法集,所有的方法保存在fun里。雖然fun大小為1,但是這其實是一個柔型數組,后面的地址空間連續且安全,
后面能存多少函數,取決於itab初始化多大的空間。

圖解iface結構
3.jpg
結合代碼,具象化的繪制一下
4.jpg
以上就是interface內部結構和動態調用的原理。根據fun方法集動態調用具體類的方法,從而實現了多態。

gdb調試

除了可以通過反匯編的方式查看代碼指令,其實通過匯編查看函數調用也是很不錯的手段。
go build -gcflags “-N -l” -o ifacedemo ifacedemo.go
先編譯出可執行文件, -N 忽略優化
然后gdb調試
gdb ifacedemo
進入gdb調試界面

1
2
3
4
5
6
7
8
9
10
11
 (gdb) list 20
15 }
16
17 func main() {
18
19 emps := EmpStruct{num: 1}
20 var empi EmpInter
21 empi = &emps
22 fmt.Println(empi)
23 fmt.Println(emps)
24

 

輸入list 20 為了列舉 20行左右代碼,然后我們在22行處打個斷點,執行run讓程序跑在斷點處停止

1
2
3
4
5
6
7
8
9
10
(gdb) break 22
Breakpoint 1 at 0x4872ea: file /home/secondtonone/workspace/goProject/src/golang-/ifacedemo/ifacedemo.go, line 22.
(gdb) run
Starting program: /home/secondtonone/workspace/goProject/src/golang-/ifacedemo/ifacedemo
[New LWP 9562]
[New LWP 9563]
[New LWP 9564]

Thread 1 "ifacedemo" hit Breakpoint 1, main.main () at /home/secondtonone/workspace/goProject/src/golang-/ifacedemo/ifacedemo.go:22
22 fmt.Println(empi)

 

接下來我們查看下empi這個接口的數據信息

1
2
(gdb) p empi
$1 = {tab = 0x4d0ee0 <EmpStruct,main.EmpInter>, data = 0xc000078010}

 

看得出empi是iface結構的,包含tab和data兩個字段。
我們查看下tab里的內容

1
2
3
4
(gdb) p empi.tab 
$2 = (runtime.itab *) 0x4d0ee0 <EmpStruct,main.EmpInter>
(gdb) p *empi.tab
$3 = {inter = 0x4a17a0, _type = 0x49f3c0, hash = 4144246241, _ = "\000\000\000", fun = {4747840}}

 

可以看得出 tab是個地址,我們*解引用看到內部內容和上面所述一樣,inter, _type, hash, 函數集合fun
接下來我們將代碼反匯編

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
 (gdb) disass
Dump of assembler code for function main.main:
0x0000000000487260 <+0>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000487269 <+9>: lea -0x58(%rsp),%rax
0x000000000048726e <+14>: cmp 0x10(%rcx),%rax
0x0000000000487272 <+18>: jbe 0x487443 <main.main+483>
0x0000000000487278 <+24>: sub $0xd8,%rsp
0x000000000048727f <+31>: mov %rbp,0xd0(%rsp)
0x0000000000487287 <+39>: lea 0xd0(%rsp),%rbp
0x000000000048728f <+47>: lea 0x1b22a(%rip),%rax # 0x4a24c0
0x0000000000487296 <+54>: mov %rax,(%rsp)
0x000000000048729a <+58>: callq 0x40b070 <runtime.newobject>
0x000000000048729f <+63>: mov 0x8(%rsp),%rax
0x00000000004872a4 <+68>: mov %rax,0x68(%rsp)
0x00000000004872a9 <+73>: movq $0x0,0x30(%rsp)
0x00000000004872b2 <+82>: movq $0x1,0x30(%rsp)
0x00000000004872bb <+91>: mov 0x68(%rsp),%rax
0x00000000004872c0 <+96>: movq $0x1,(%rax)
0x00000000004872c7 <+103>: xorps %xmm0,%xmm0
0x00000000004872ca <+106>: movups %xmm0,0x70(%rsp)
0x00000000004872cf <+111>: mov 0x68(%rsp),%rax
0x00000000004872d4 <+116>: mov %rax,0x50(%rsp)
0x00000000004872d9 <+121>: lea 0x49c00(%rip),%rcx # 0x4d0ee0 <go.itab.*main.EmpStruct,main.EmpInter>
0x00000000004872e0 <+128>: mov %rcx,0x70(%rsp)
0x00000000004872e5 <+133>: mov %rax,0x78(%rsp)
0x00000000004872ea <+138>: mov 0x78(%rsp),%rax
0x00000000004872ef <+143>: mov 0x70(%rsp),%rcx
0x00000000004872f4 <+148>: mov %rcx,0x80(%rsp)
0x00000000004872fc <+156>: mov %rax,0x88(%rsp)
0x0000000000487304 <+164>: mov %rcx,0x48(%rsp)
0x0000000000487309 <+169>: cmpq $0x0,0x48(%rsp)
0x000000000048730f <+175>: jne 0x487316 <main.main+182>
0x0000000000487311 <+177>: jmpq 0x48743e <main.main+478>
0x0000000000487316 <+182>: test %al,(%rcx)
0x0000000000487318 <+184>: mov 0x8(%rcx),%rax
0x000000000048731c <+188>: mov %rax,0x48(%rsp)
0x0000000000487321 <+193>: jmp 0x487323 <main.main+195>
0x0000000000487323 <+195>: xorps %xmm0,%xmm0
0x0000000000487326 <+198>: movups %xmm0,0x90(%rsp)
0x000000000048732e <+206>: lea 0x90(%rsp),%rax
0x0000000000487336 <+214>: mov %rax,0x40(%rsp)
0x000000000048733b <+219>: test %al,(%rax)
0x000000000048733d <+221>: mov 0x48(%rsp),%rcx
0x0000000000487342 <+226>: mov 0x88(%rsp),%rdx
0x000000000048734a <+234>: mov %rcx,0x90(%rsp)
0x0000000000487352 <+242>: mov %rdx,0x98(%rsp)
0x000000000048735a <+250>: test %al,(%rax)
0x000000000048735c <+252>: jmp 0x48735e <main.main+254>
0x000000000048735e <+254>: mov %rax,0xa0(%rsp)
0x0000000000487366 <+262>: movq $0x1,0xa8(%rsp)
0x0000000000487372 <+274>: movq $0x1,0xb0(%rsp)
0x000000000048737e <+286>: mov %rax,(%rsp)
0x0000000000487382 <+290>: movq $0x1,0x8(%rsp)
0x000000000048738b <+299>: movq $0x1,0x10(%rsp)
0x0000000000487394 <+308>: callq 0x480c60 <fmt.Println>
=> 0x0000000000487399 <+313>: mov 0x68(%rsp),%rax
0x000000000048739e <+318>: mov (%rax),%rax
0x00000000004873a1 <+321>: mov %rax,0x38(%rsp)
0x00000000004873a6 <+326>: mov %rax,(%rsp)
0x00000000004873aa <+330>: callq 0x408950 <runtime.convT64>
0x00000000004873af <+335>: mov 0x8(%rsp),%rax
---Type <return> to continue, or q <return> to quit---

 

顯示的就是在main包main函數的反匯編。和我們之前用tool工具看到的一樣。
到此為止,interface內部結構和特性介紹完畢
感謝關注我的公眾號
wxgzh.jpg


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM