Go語言interface實現原理詳解


1 前言

1.1 Go匯編

 Go語言被定義為一門系統編程語言,與C語言一樣通過編譯器生成可直接運行的二進制文件。這一點與Java,PHP,Python等編程語言存在很大的不同,這些語言都是運行在基於C語言開發的虛擬機上,如果想深入了解運行原理只需要看懂對應的C語言開發的虛擬機(絕大部分程序員應該都對C語言有基本的了解)。但是如果想深入學習Go語言,就需要對基本的匯編指令和語法有一定的了解(通過匯編可以了解到編譯器到底做了什么工作)
 通過下面的例子簡單了解如何通過匯編來了解Go語言的運行原理。編輯一個go文本call_function.go,輸入如下代碼:

     1 package main 2 3 func add(a, b int) int { 4 return a + b 5 } 6 7 func main() { 8 a := 10 9 b := 20 10 11 c := add(a, b) 12 _ = c 13 } 

 輸入命令go build -gcflags '-l -N' call_function.go生成可執行文件,然后輸入命令go tool objdump -s "main.main" call_function查看匯編代碼如下:

     1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/call_function.go 2 call_function.go:7 0x104f380 65488b0c25a0080000 MOVQ GS:0x8a0, CX 3 call_function.go:7 0x104f389 483b6110 CMPQ 0x10(CX), SP 4 call_function.go:7 0x104f38d 764c JBE 0x104f3db 5 call_function.go:7 0x104f38f 4883ec38 SUBQ $0x38, SP 6 call_function.go:7 0x104f393 48896c2430 MOVQ BP, 0x30(SP) 7 call_function.go:7 0x104f398 488d6c2430 LEAQ 0x30(SP), BP 8 call_function.go:8 0x104f39d 48c74424280a000000 MOVQ $0xa, 0x28(SP) 9 call_function.go:9 0x104f3a6 48c744242014000000 MOVQ $0x14, 0x20(SP) 10 call_function.go:11 0x104f3af 488b442428 MOVQ 0x28(SP), AX 11 call_function.go:11 0x104f3b4 48890424 MOVQ AX, 0(SP) 12 call_function.go:11 0x104f3b8 488b442420 MOVQ 0x20(SP), AX 13 call_function.go:11 0x104f3bd 4889442408 MOVQ AX, 0x8(SP) 14 call_function.go:11 0x104f3c2 e899ffffff CALL main.add(SB) 15 call_function.go:11 0x104f3c7 488b442410 MOVQ 0x10(SP), AX 16 call_function.go:11 0x104f3cc 4889442418 MOVQ AX, 0x18(SP) 17 call_function.go:13 0x104f3d1 488b6c2430 MOVQ 0x30(SP), BP 18 call_function.go:13 0x104f3d6 4883c438 ADDQ $0x38, SP 19 call_function.go:13 0x104f3da c3 RET 20 call_function.go:7 0x104f3db e89083ffff CALL runtime.morestack_noctxt(SB) 21 call_function.go:7 0x104f3e0 eb9e JMP main.main(SB) 

 第8~9行匯編代碼,分別將SP(棧寄存器)偏移0x28和0x20的地址賦值為0xa和0x14,對應Go代碼的第8行和第9行中的對a,b變量賦值,也就是說a變量對應的內存地址是SP+0x28,b變量對應的內存地址是SP+0x20。
 然后10~14行匯編代碼表示對a,b變量進行拷貝,分別拷貝到SP+0x0和SP+0x8地址,然后調用add方法,這就是通常說到的函數調用時的“值傳遞”。
 輸入命令go tool objdump -s "main.add" call_function,可以看到如下的匯編代碼:

     1 TEXT main.add(SB) /Users/didi/Source/Go/src/ppt/call_function.go 2 call_function.go:3 0x104f360 48c744241800000000 MOVQ $0x0, 0x18(SP) 3 call_function.go:4 0x104f369 488b442408 MOVQ 0x8(SP), AX 4 call_function.go:4 0x104f36e 4803442410 ADDQ 0x10(SP), AX 5 call_function.go:4 0x104f373 4889442418 MOVQ AX, 0x18(SP) 6 call_function.go:4 0x104f378 c3 RET 

 第3~5行匯編代碼表示,將SP+0x8和SP+0x10地址的值相加,並復制到SP+0x18地址。
 為什么在main函數中,a和b變量分別復制到了SP+0x0和SP+0x8地址,但是在add函數中,卻將SP+0x8和SP+0x10地址的值進行相加呢?
 這是因為在main函數中的匯編代碼14行中,調用call執行時CPU會執行一次壓棧操作,將函數調用完成以后需要返回的地址存在SP-0x8的地址處,並執行一次SP=SP-0x8的操作(具體操作可以百度一下)。所以在add函數里面的SP+0x8和SP+0x10地址就對應着main函數中的SP+0x0和SP+0x8地址。
 具體過程如下圖:

 
go函數調用.jpg

 

1.2 Go指針

 Go的庫代碼中大量使用了一些指針進行內存操作。但是在Go語言中指針變量是不能進行運算的,所以不能像C語言那樣方便的對內存進行偏移尋址,但是Go中提供了unsafe包來對指針計算運算。
 下面的例子可以說明使用方式:

     1 package main 2 3 import ( 4 "fmt" 5 "unsafe" 6 ) 7 8 type Struct1 struct { 9 A int64 10 B int64 11 C int64 12 } 13 14 type Struct2 struct { 15 A int64 16 B int64 17 C int64 18 } 19 20 func main() { 21 struct1 := Struct1 { 22 A : 1, 23 B : 2, 24 C : 3, 25 } 26 27 struct2 := new(Struct2) 28 29 var src uintptr = uintptr(unsafe.Pointer(&struct1)) 30 var dst uintptr = uintptr(unsafe.Pointer(struct2)) 31 for i := 0; i < 24; i++ { 32 *(*uint8)(unsafe.Pointer(dst + uintptr(i))) = *(*uint8)(unsafe.Pointer(src + uintptr(i))) 33 } 34 35 fmt.Println("struct1=%v||struct2=%v", struct1, *struct2); 36 } 

 在上面的例子將struct1對應內存的值復制到struct2對應的內存中,從例子中可以看出可以看到Go語言中

  • unsafe.Pointer類似於C中的void*,任何類型的指針都可以轉換為unsafe.Pointer 類型,unsafe.Pointer 類型也可以轉換為任何指針類型;
  • uintptr可以存go中的任何變量,如果想對指針進行運算,必須先把指針轉換為uintptr。

2 Go的interface的實現

 在Go語言中interface是一個非常重要的概念,也是與其它語言相比存在很大特色的地方。interface也是一個Go語言中的一種類型,是一種比較特殊的類型,存在兩種interface,一種是帶有方法的interface,一種是不帶方法的interface。Go語言中的所有變量都可以賦值給空interface變量,實現了interface中定義方法的變量可以賦值給帶方法的interface變量,並且可以通過interface直接調用對應的方法,實現了其它面向對象語言的多態的概念。

2.1 內部定義

 兩種不同的interface在Go語言內部被定義成如下的兩種結構體(源碼基於Go的1.9.2版本)

// 沒有方法的interface type eface struct { _type *_type data unsafe.Pointer } // 記錄着Go語言中某個數據類型的基本特征 type _type struct { size uintptr ptrdata uintptr hash uint32 tflag tflag align uint8 fieldalign uint8 kind uint8 alg *typeAlg gcdata *byte str nameOff ptrToThis typeOff } // 有方法的interface type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter *interfacetype _type *_type link *itab hash uint32 bad bool inhash bool unused [2]byte fun [1]uintptr } // interface數據類型對應的type type interfacetype struct { typ _type pkgpath name mhdr []imethod } 

 可以看到兩種類型的interface在內部實現時都是定義成了一個2個字段的結構體,所以任何一個interface變量都是占用16個byte的內存空間。
在Go語言中_type這個結構體非常重要,記錄着某種數據類型的一些基本特征,比如這個數據類型占用的內存大小(size字段),數據類型的名稱(nameOff字段)等等。每種數據類型都存在一個與之對應的_type結構體(Go語言原生的各種數據類型,用戶自定義的結構體,用戶自定義的interface等等)。如果是一些比較特殊的數據類型,可能還會對_type結構體進行擴展,記錄更多的信息,比如interface類型,就會存在一個interfacetype結構體,除了通用的_type外,還包含了另外兩個字段pkgpath和mhdr,后文在對這兩個字段的作用進行解析。除此之外還有其它類型的數據結構對應的結構體,比如structtype,chantype,slicetype,有興趣的可以在$GOROOT/src/runtime/type.go文件中查看。

 
iface和eface的內存分布.jpg

 

2.2 賦值

 存在對沒有方法的interface變量和有方法的interface變量賦值這兩種不同的情況。分別詳解這兩種不同的賦值過程。

  • 沒有方法的interface變量賦值
     對沒有方法的interface變量賦值時編譯器做了什么工作?創建一個eface.go文件,代碼如下:
     1 package main 2 3 type Struct1 struct { 4 A int64 5 B int64 6 } 7 8 func main() { 9 s := new(Struct1) 10 var i interface{} 11 i = a 12 13 _ = i 14 } 

 輸入命令go build -gcflags '-l -N' eface.go,go tool objdump -s "main.main" eface,查看匯編代碼。

     1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/eface.go 2 eface.go:8 0x104f360 4883ec38 SUBQ $0x38, SP 3 eface.go:8 0x104f364 48896c2430 MOVQ BP, 0x30(SP) 4 eface.go:8 0x104f369 488d6c2430 LEAQ 0x30(SP), BP 5 eface.go:9 0x104f36e 48c7042400000000 MOVQ $0x0, 0(SP) 6 eface.go:9 0x104f376 48c744240800000000 MOVQ $0x0, 0x8(SP) 7 eface.go:9 0x104f37f 488d0424 LEAQ 0(SP), AX 8 eface.go:9 0x104f383 4889442410 MOVQ AX, 0x10(SP) 9 eface.go:10 0x104f388 48c744242000000000 MOVQ $0x0, 0x20(SP) 10 eface.go:10 0x104f391 48c744242800000000 MOVQ $0x0, 0x28(SP) 11 eface.go:11 0x104f39a 488b442410 MOVQ 0x10(SP), AX 12 eface.go:11 0x104f39f 4889442418 MOVQ AX, 0x18(SP) 13 eface.go:11 0x104f3a4 488d0dd5670000 LEAQ 0x67d5(IP), CX 14 eface.go:11 0x104f3ab 48894c2420 MOVQ CX, 0x20(SP) 15 eface.go:11 0x104f3b0 4889442428 MOVQ AX, 0x28(SP) 16 eface.go:14 0x104f3b5 488b6c2430 MOVQ 0x30(SP), BP 17 eface.go:14 0x104f3ba 4883c438 ADDQ $0x38, SP 

 匯編代碼第5~6行給結構體Struct1分配了空間SP+0x0和SP+0x8,第7~8行把這個結構體的地址放在存入了SP+0x10地址,這個地址就是變量s,第9~10行給interface類型的變量i分配了SP+0x20和SP+0x28,第13~14行把結構體A對應的_type的地址賦值到SP+0x20,然后把a變量賦值到了SP+0x28。這就是對沒有方法的interface進行賦值的過程。賦值完以后的內存分配如下圖:


 
沒有方法的interface賦值.jpg
  • 有方法的interface變量賦值
     如下一段代碼在內存的分布
     1 package main 2 3 type I interface { 4 Add() 5 Del() 6 } 7 8 type Struct1 struct { 9 A int64 10 B int64 11 } 12 13 func (a *Struct1) Add() { 14 a.A = a.A + 1 15 a.B = a.B + 1 16 } 17 18 func (a *Struct1) Del() { 19 a.A = a.A - 1 20 a.B = a.B - 1 21 } 22 23 func main() { 24 a := new(Struct1) 25 var i I 26 i = a 27 28 i.Add() 29 i.Del() 30 } 
 
有方法的interface賦值.jpg

 這些內存地址都可以使用gdb調試時得到

(gdb) p i
$11 = {tab = 0x10a70e0 <Struct1,main.I>, data = 0xc42001a0c0}
(gdb) p a
$12 = (struct main.Struct1 *) 0xc42001a0c0
(gdb) p i.tab
$13 = (runtime.itab *) 0x10a70e0 <Struct1,main.I>
(gdb) p i.tab.inter
$14 = (runtime.interfacetype *) 0x105dc60 <type.*+59232>
(gdb) p i.tab._type
$15 = (runtime._type *) 0x105d200 <type.*+56576>

 通過對內存地址的打印,可以很清晰的看出在對有方法的interface變量進行賦值時的內存分布。Struct1類型和interface I類型都存在內存記錄着各自的_type結構體信息,在將Struct1類型的變量賦值給interface I類型時,會有一個itab類型的結構體將Struct1類型和interface I類型關聯起來。
上面的例子都是將一個指針賦值給interface變量,如果是將一個值賦值給interface變量。會先對分配一塊空間保存該值的副本,然后將該interface變量的data字段指向這個新分配的空間。將一個值賦值給interface變量時,操作的都是該值的一個副本。

2.3 方法的調用

 上面對有方法的interface進行賦值后,是如何實現通過接口變量實現了函數調用呢?參考下面的匯編代碼

     1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/iface.go 2 iface.go:23 0x104f3e0 65488b0c25a0080000 MOVQ GS:0x8a0, CX 3 iface.go:23 0x104f3e9 483b6110 CMPQ 0x10(CX), SP 4 iface.go:23 0x104f3ed 0f8687000000 JBE 0x104f47a 5 iface.go:23 0x104f3f3 4883ec38 SUBQ $0x38, SP 6 iface.go:23 0x104f3f7 48896c2430 MOVQ BP, 0x30(SP) 7 iface.go:23 0x104f3fc 488d6c2430 LEAQ 0x30(SP), BP 8 iface.go:23 0x104f401 488d0578ff0000 LEAQ 0xff78(IP), AX 9 iface.go:24 0x104f408 48890424 MOVQ AX, 0(SP) 10 iface.go:24 0x104f40c e86fcefbff CALL runtime.newobject(SB) 11 iface.go:24 0x104f411 488b442408 MOVQ 0x8(SP), AX 12 iface.go:24 0x104f416 4889442410 MOVQ AX, 0x10(SP) 13 iface.go:25 0x104f41b 48c744242000000000 MOVQ $0x0, 0x20(SP) 14 iface.go:25 0x104f424 48c744242800000000 MOVQ $0x0, 0x28(SP) 15 iface.go:26 0x104f42d 488b442410 MOVQ 0x10(SP), AX 16 iface.go:26 0x104f432 4889442418 MOVQ AX, 0x18(SP) 17 iface.go:26 0x104f437 488d0da27c0500 LEAQ 0x57ca2(IP), CX 18 iface.go:26 0x104f43e 48894c2420 MOVQ CX, 0x20(SP) 19 iface.go:26 0x104f443 4889442428 MOVQ AX, 0x28(SP) 20 iface.go:28 0x104f448 488b442420 MOVQ 0x20(SP), AX 21 iface.go:28 0x104f44d 488b4020 MOVQ 0x20(AX), AX 22 iface.go:28 0x104f451 488b4c2428 MOVQ 0x28(SP), CX 23 iface.go:28 0x104f456 48890c24 MOVQ CX, 0(SP) 24 iface.go:28 0x104f45a ffd0 CALL AX 25 iface.go:29 0x104f45c 488b442420 MOVQ 0x20(SP), AX 26 iface.go:29 0x104f461 488b4028 MOVQ 0x28(AX), AX 27 iface.go:29 0x104f465 488b4c2428 MOVQ 0x28(SP), CX 28 iface.go:29 0x104f46a 48890c24 MOVQ CX, 0(SP) 29 iface.go:29 0x104f46e ffd0 CALL AX 30 iface.go:30 0x104f470 488b6c2430 MOVQ 0x30(SP), BP 31 iface.go:30 0x104f475 4883c438 ADDQ $0x38, SP 32 iface.go:30 0x104f479 c3 RET 33 iface.go:23 0x104f47a e8f182ffff CALL runtime.morestack_noctxt(SB) 34 iface.go:23 0x104f47f e95cffffff JMP main.main(SB) 

 匯編代碼的第17行和18行,將itab的地址加載到SP+0x20地址處,第20,21行,24行將SP+0x20的值加載到AX寄存器,然后將AX+0x20地址的值加載到AX寄存器,CALL AX就實現了add方法的調用,其中第22行和23行的作用是將interface里面data字段的地址傳遞給了add方法。


 
iface函數調用.jpg

 通過對itab結構體進行分析,可以看到偏移0x20處為fun字段,其中0x20處為add函數的入口地址,0x28處就是del函數的入口地址。

2.4 斷言的實現

 在Go語言中,經常需要對一個interface變量進行斷言

     1 package main 2 3 type Struct1 struct { 4 A int64 5 } 6 7 func main() { 8 a := new(Struct1) 9 10 var i interface{} 11 i = a 12 13 b, ok := i.(Struct1) 14 if ok { 15 _ = b 16 } 17 } 

 生成匯編代碼進行分析

     1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/assert.go 2 assert.go:7 0x104f360 4883ec48 SUBQ $0x48, SP 3 assert.go:7 0x104f364 48896c2440 MOVQ BP, 0x40(SP) 4 assert.go:7 0x104f369 488d6c2440 LEAQ 0x40(SP), BP 5 assert.go:8 0x104f36e 48c744241000000000 MOVQ $0x0, 0x10(SP) 6 assert.go:8 0x104f377 488d442410 LEAQ 0x10(SP), AX 7 assert.go:8 0x104f37c 4889442420 MOVQ AX, 0x20(SP) 8 assert.go:10 0x104f381 48c744243000000000 MOVQ $0x0, 0x30(SP) 9 assert.go:10 0x104f38a 48c744243800000000 MOVQ $0x0, 0x38(SP) 10 assert.go:11 0x104f393 488b442420 MOVQ 0x20(SP), AX 11 assert.go:11 0x104f398 4889442428 MOVQ AX, 0x28(SP) 12 assert.go:11 0x104f39d 488d0d1c680000 LEAQ 0x681c(IP), CX 13 assert.go:11 0x104f3a4 48894c2430 MOVQ CX, 0x30(SP) 14 assert.go:11 0x104f3a9 4889442438 MOVQ AX, 0x38(SP) 15 assert.go:13 0x104f3ae 488b442438 MOVQ 0x38(SP), AX 16 assert.go:13 0x104f3b3 488b4c2430 MOVQ 0x30(SP), CX 17 assert.go:13 0x104f3b8 488d1581ed0000 LEAQ 0xed81(IP), DX 18 assert.go:13 0x104f3bf 4839d1 CMPQ DX, CX 19 assert.go:13 0x104f3c2 7402 JE 0x104f3c6 20 assert.go:13 0x104f3c4 eb3f JMP 0x104f405 21 assert.go:13 0x104f3c6 488b00 MOVQ 0(AX), AX 22 assert.go:13 0x104f3c9 b901000000 MOVL $0x1, CX 23 assert.go:13 0x104f3ce eb00 JMP 0x104f3d0 

 匯編的第12行,17行,18行可以看出,將Struct1對應的_type結構體的地址賦值給interface以后。在進行斷言的時候,原理就是將interface變量_type字段的與Struct1對應的_type結構地址進行對比。
在本例子中,第12行的IP寄存器對應的值是0x104f39d,0x681c(IP)對應的地址為0x1055BB9,第17行的IP寄存器對應的值是0x104f3b8,0xed81(IP)對應的地址為0x105E139,貌似並不相同。可能是對Go的匯編中對IP寄存器的理解存在偏差,找了幾個小時資料都沒找到原因。

3 Go的反射

 反射是一種強大的語言特性,可以“動態”的調用方法,獲取結構體運行時的一些特征,很多框架的實現都離不開反射。Go的反射就是通過interface類型來實現的。

3.1 反射獲取變量的信息

 Go的反射包主要存在兩個重要的結構體。

     1 type Value struct { 2 typ *rtype 3 ptr unsafe.Pointer 4 flag 5 } 6 7 func ValueOf(i interface{}) Value { 8 } 9 10 type Type interface { 11 Align() int 12 FieldAlign() int 13 Method(int) Method 14 Name() string 15 //一堆方法 16 //.... 17 } 18 19 func TypeOf(i interface{}) Type { 20 eface := *(*emptyInterface)(unsafe.Pointer(&i)) 21 return toType(eface.typ) 22 } 23 24 type emptyInterface struct { 25 typ *rtype 26 word unsafe.Pointer 27 } 

 任何一個變量可以通過調用ValueOf來獲取到變量的Value結構體,通過TypeOf方法來獲取變量的Type接口類型。通過TypeOf方法獲取到的Type接口實際上就是該變量對應的_type。
 通過前面的分析,當通過TypeOf方法獲取到變量的_type結構體后,很容易獲取到該變量的一些基本信息,比如_type結構體中的各種字段都可以直接獲取到。

3.2 反射修改變量的值

     1 package main 2 3 import ( 4 "reflect" 5 ) 6 7 func main() { 8 var x int64 = 10 9 10 reflect.ValueOf(x).SetInt(20) 11 12 reflect.ValueOf(&x).SetInt(20) 13 14 reflect.ValueOf(&x).Elem().SetInt(20) 15 } 

 上面的例子中,第10行,12行都會報panic,只有第14行能修改變量的值。在使用ValueOf獲取到Value結構體以后,flag字段記錄着值能否進行修改,這樣應該是為了避免誤操作,保證api調用者明確了解到是否需要修改值。

3.3 反射修改結構體變量字段的值

 如果需要通過反射修改某結構體里面各個字段的值。

     1 package main 2 3 import ( 4 "reflect" 5 "fmt" 6 ) 7 8 type Struct1 struct { 9 A int64 10 B int64 11 C int64 12 } 13 14 func main() { 15 P := new(Struct1) 16 17 V := reflect.ValueOf(P).Elem() 18 V.FieldByName("A").SetInt(100) 19 V.FieldByName("B").SetInt(200) 20 V.FieldByName("C").SetInt(300) 21 22 fmt.Printf("%v", P) 23 } 

 上面的代碼中,需要根據結構體字段的名稱對各個字段的值進行修改,內部是如何實現的呢?


 
自定義struct內存分布.jpg

 每一個自定義的struct類型都存在這一個對應的structType結構體,該結構體記錄了每個字段structField。通過對比structField里面的name字段,就可以獲取到某個字段的type和偏移量。從而對具體的值進行修改。

3.4 反射動態調用方法

 動態的調用方法是怎么實現的?

     1 package main 2 3 import ( 4 "reflect" 5 ) 6 7 type Struct1 struct { 8 A int64 9 B int64 10 C int64 11 } 12 13 func (p *Struct1) Set() { 14 p.A = 200 15 } 16 17 func main() { 18 P := new(Struct1) 19 P.A = 100 20 P.B = 200 21 P.C = 300 22 23 V := reflect.ValueOf(P) 24 25 params := make([]reflect.Value, 0) 26 V.MethodByName("Set").Call(params) 27 } 

 結構體的方法在內存中存在如下的分布


 
反射獲取方法.jpg

 在編譯過程中,結構體對應方法的相關信息都已經存在於內存中,分配了一塊uncommonType的結構體跟在fields字段后面。根據內存的分布,如果需要根據一個結構體的名稱獲取到方法並且執行,只需要根據uncommonType結構中的moff字段去獲取方法相關信息的地址塊,然后逐個對比名稱是否為想要獲取的方法進行調用。

4 總結

 本文從實現原理上分析了Go語言中interface類型和反射包的使用,相信各位讀者以后再使用Go的interface類型和反射包時能做到胸有成竹,也能夠對分析Go語言的其它特性提供思路。



作者:喻家山車神
鏈接:https://www.jianshu.com/p/70003e0f49d1
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

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



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