一篇明白內(nèi)存中的接口類型
前言
抽象來講,接口,是一種約定,是一種約束,是一種協(xié)議。
在Go語言中,接口是一種語法類型,用來定義一種編程規(guī)范。
在Go語言中,接口主要有兩類:
沒有方法定義的空接口
有方法定義的非空接口
之前,有兩篇圖文詳細介紹了空接口對象及其類型:
- 【Go】內(nèi)存中的空接口
- 【Go】再談空接口
本文將深入探究包含方法的非空接口,以下簡稱接口。
環(huán)境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
聲明
操作系統(tǒng)、處理器架構(gòu)、Go版本不同,均有可能造成相同的源碼編譯后運行時的寄存器值、內(nèi)存地址、數(shù)據(jù)結(jié)構(gòu)等存在差異。
本文僅包含 64 位系統(tǒng)架構(gòu)下的 64 位可執(zhí)行程序的研究分析。
本文僅保證學(xué)習(xí)過程中的分析數(shù)據(jù)在當(dāng)前環(huán)境下的準確有效性。
代碼清單
- // interface_in_memory.go
- package main
- import "fmt"
- import "reflect"
- import "strconv"
- type foo interface {
- fmt.Stringer
- Foo()
- ree()
- }
- type fooImpl int
- //go:noinline
- func (i fooImpl) Foo() {
- println("hello foo")
- }
- //go:noinline
- func (i fooImpl) ree() {
- println("hello ree")
- }
- //go:noinline
- func (i fooImpl) String() string {
- return strconv.Itoa(int(i))
- }
- func main() {
- impl := fooImpl(123)
- impl.Foo()
- impl.ree()
- fmt.Println(impl.String())
- typeOf(impl)
- exec(impl)
- }
- //go:noinline
- func exec(foo foo) {
- foo.Foo()
- foo.ree()
- fmt.Println(foo.String())
- typeOf(foo)
- fmt.Printf("exec 參數(shù)類型地址:%p\n", reflect.TypeOf(exec).In(0))
- }
- //go:noinline
- func typeOf(i interface{}) {
- v := reflect.ValueOf(i)
- t := v.Type()
- fmt.Printf("類型:%s\n", t.String())
- fmt.Printf("地址:%p\n", t)
- fmt.Printf("值 :%d\n", v.Int())
- fmt.Println()
- }
以上代碼,定義了一個包含3個方法的接口類型foo,還定義了一個fooImpl類型。在語法上,我們稱fooImpl類型實現(xiàn)了foo接口。
運行結(jié)果
程序結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)介紹
接口數(shù)據(jù)類型的結(jié)構(gòu)定義在reflect/type.go源文件中,如下所示:
- // 表示一個接口方法
- type imethod struct {
- name nameOff // 方法名稱相對程序 .rodata 節(jié)的偏移量
- typ typeOff // 方法類型相對程序 .rodata 節(jié)的偏移量
- }
- // 表示一個接口數(shù)據(jù)類型
- type interfaceType struct {
- rtype // 基礎(chǔ)信息
- pkgPath name // 包路徑信息
- methods []imethod // 接口方法
- }
其實這只是一個表象,完整的接口數(shù)據(jù)類型結(jié)構(gòu)如下偽代碼所示:
- // 表示一個接口類型
- type interfaceType struct {
- rtype // 基礎(chǔ)信息
- pkgPath name // 包路徑信息
- methods []imethod // 接口方法的 slice,實際指向 array 字段
- u uncommonType // 占位
- array [len(methods)]imethod // 實際的接口方法數(shù)據(jù)
- }
完整的結(jié)構(gòu)分布圖如下:
另外兩個需要了解的結(jié)構(gòu)體,之前文章已經(jīng)多次介紹過,也在reflect/type.go源文件中,定義如下:
- type uncommonType struct {
- pkgPath nameOff // 包路徑名稱偏移量
- mcount uint16 // 方法的數(shù)量
- xcount uint16 // 公共導(dǎo)出方法的數(shù)量
- moff uint32 // [mcount]method 相對本對象起始地址的偏移量
- _ uint32 // unused
- }
reflect.uncommonType結(jié)構(gòu)體用于描述一個數(shù)據(jù)類型的包名和方法信息。對于接口類型,意義不是很大。
- // 非接口類型的方法
- type method struct {
- name nameOff // 方法名稱偏移量
- mtyp typeOff // 方法類型偏移量
- ifn textOff // 通過接口調(diào)用時的地址偏移量;接口類型本文不介紹
- tfn textOff // 直接類型調(diào)用時的地址偏移量
- }
reflect.method結(jié)構(gòu)體用于描述一個非接口類型的方法,它是一個壓縮格式的結(jié)構(gòu),每個字段的值都是一個相對偏移量。
- type nameOff int32 // offset to a name
- type typeOff int32 // offset to an *rtype
- type textOff int32 // offset from top of text section
- nameOff 是相對程序 .rodata 節(jié)起始地址的偏移量。
- typeOff 是相對程序 .rodata 節(jié)起始地址的偏移量。
- textOff 是相對程序 .text 節(jié)起始地址的偏移量。
接口實現(xiàn)類型
從以上“運行結(jié)果”可以看到,fooImpl的類型信息位于0x4a9be0內(nèi)存地址處。
關(guān)于fooImpl類型,【Go】再談?wù)麛?shù)類型一文曾進行過非常詳細的介紹,此處僅分析其方法相關(guān)內(nèi)容。
查看fooImpl類型的內(nèi)存數(shù)據(jù)如下:
繪制成圖表如下:
fooImpl類型有3個方法,我們以Foo方法來說明接口相關(guān)的底層原理。
Foo方法的相關(guān)數(shù)據(jù)如下:
- var Foo = reflect.method {
- name: 0x00000172, // 方法名稱相對程序 `.rodata` 節(jié)起始地址的偏移量
- mtyp: 0x00009960, // 方法類型相對程序 `.rodata` 節(jié)起始地址的偏移量
- ifn: 0x000989a0, // 接口調(diào)用的指令相對程序 `.text` 節(jié)起始地址的偏移量
- tfn: 0x00098160, // 正常調(diào)用的指令相對程序 `.text` 節(jié)起始地址的偏移量
- }
方法名稱
method.name用于定位方法的名稱,即一個reflect.name對象。
Foo方法的reflect.name對象位于 0x49a172(0x00000172 + 0x49a000)地址處,毫無疑問,解析結(jié)果是Foo。
- (gdb) p /x 0x00000172 + 0x49a000
- $3 = 0x49a172
- (gdb) x /3bd 0x49a172
- 0x49a172: 1 0 3
- (gdb) x /3c 0x49a172 + 3
- 0x49a175: 70 'F' 111 'o' 111 'o'
- (gdb)
方法類型
method.mtyp用于定位方法的數(shù)據(jù)類型,即一個reflect.funcType對象。
Foo方法的reflect.funcType對象,其位于0x4a3960(0x00009960 + 0x49a000)地址處。
Foo方法的數(shù)據(jù)類型的字符串表示形式是func()。
- (gdb) x /56bx 0x4a3960
- 0x4a3960: 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- 0x4a3968: 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- 0x4a3970: 0xf6 0xbc 0x82 0xf6 0x02 0x08 0x08 0x33
- 0x4a3978: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- 0x4a3980: 0xa0 0x4a 0x4c 0x00 0x00 0x00 0x00 0x00
- 0x4a3988: 0x34 0x11 0x00 0x00 0x00 0x00 0x00 0x00
- 0x4a3990: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- (gdb) x /wx 0x4a3988
- 0x4a3988: 0x00001134
- (gdb) x /s 0x00001134 + 0x49a000 + 3
- 0x49b137: "*func()"
- (gdb)
想要深入了解函數(shù)類型,請閱讀【Go】內(nèi)存中的函數(shù)。
接口方法
method.ifn字段的英文注釋為function used in interface call,即調(diào)用接口方法時使用的函數(shù)。
在本例中,就是通過foo接口調(diào)用fooImpl類型的Foo函數(shù)時需要執(zhí)行的指令集合。
具體來講就是,代碼清單中的exec函數(shù)內(nèi)調(diào)用Foo方法需要執(zhí)行的指令集合。
Foo函數(shù)的method.ifn = 0x000989a0,計算出其指令集合位于地址0x4999a0(0x000989a0 + 0x401000)處。
通過內(nèi)存數(shù)據(jù)可以清楚地看到,接口方法的符號是main.(*fooImpl).Foo。該函數(shù)主要做了兩件事:
檢查panic
在0x4999d7地址處調(diào)用另一個函數(shù)main.fooImpl.Foo。
類型方法
method.tfn字段的英文注釋為function used for normal method call,即正常方法調(diào)用時使用的函數(shù)。
在本例中,就是通過fooImpl類型的對象調(diào)用Foo函數(shù)時需要執(zhí)行的指令集合。
具體來講就是,代碼清單中的main函數(shù)內(nèi)調(diào)用Foo方法需要執(zhí)行的指令集合。
Foo函數(shù)的method.tfn = 0x00098160,計算出其指令集合位于地址0x499160(0x00098160 + 0x401000)處。
通過內(nèi)存數(shù)據(jù)可以清楚地看到,類型方法的符號是main.fooImpl.Foo。
調(diào)用堆棧
通過上述分析,已經(jīng)能夠?qū)ethod.ifn和method.tfn兩個字段的含義建立起基本的認知。
實踐是檢驗真理的唯一標準。能動手盡量別吵吵。
在main.(*fooImpl).Foo和main.fooImpl.Foo兩個函數(shù)的入口處設(shè)置斷點,通過行動鞏固我們對接口類型的認識。
通過動態(tài)調(diào)試,我們清晰地看到:
- main函數(shù)調(diào)用了main.fooImpl.Foo函數(shù)
- exec函數(shù)調(diào)用了main.(*fooImpl).Foo函數(shù)
- main.(*fooImpl).Foo函數(shù)調(diào)用了main.fooImpl.Foo函數(shù)
- main.(*fooImpl).Foo函數(shù)的調(diào)試信息顯示autogenerated,表示其是由編譯器生成的
對比本文“代碼清單”,你是否對Go語言的方法調(diào)用有了全新的認識。
幾乎每種編程語言都會存在編譯器自動生成代碼的情況,用來實現(xiàn)某些通用邏輯的處理。本例中自動生成的main.(*fooImpl).Foo函數(shù)中增加了panic檢查邏輯,不過, 乍看起來這像是某種設(shè)計缺陷導(dǎo)致不能直接調(diào)用main.fooImpl.Foo函數(shù),而是必須經(jīng)過一個"中間人"才行。
接口類型
從以上“運行結(jié)果”可以看到,exec函數(shù)的參數(shù)類型的地址是0x4aa5c0,也就是foo接口的類型信息存儲位置。查看類型數(shù)據(jù)如下:
將以上內(nèi)存數(shù)據(jù)繪制成圖表如下:
- rtype.size = 16
- rtype.ptrdata = 16
- rtype.hash = 0x187f135e
- rtype.tflag = 0xf = reflect.tflagUncommon | reflect.tflagExtraStar | reflect.tflagNamed
- rtype.align = 8
- rtype.fieldAlign = 8
- rtype.kind = 0x14 = 20 = reflect.Interface
- rtype.equal = 0x4c4d38 -> runtime.interequal
- rtype.str = 0x000003e3 -> *main.foo
- rtype.ptrToThis = 0x00006a20 -> *foo
- interfaceType.pkgPath = 0x49a34c -> main
- interfaceType.methods.Data = 0x4aa620
- interfaceType.methods.Len = 3
- interfaceType.methods.Cap = 3
- uncommonType.pkgPath = 0x0000034c
- uncommonType.mcount = 0
- uncommonType.xcount = 0
- uncommonType.moff = 0x28
- interfaceType.methods[0].name = 0x00000172 -> Foo
- interfaceType.methods[0].typ = 0x00009960 -> func()
- interfaceType.methods[1].name = 0x00000d7a -> String
- interfaceType.methods[1].typ = 0x0000a140 -> func() string
- interfaceType.methods[2].name = 0x000002ce -> ree
- interfaceType.methods[2].typ = 0x00009960 -> func()
對象大小
接口類型的對象大小(rtype.size)是16字節(jié),指針數(shù)據(jù)(rtype.ptrdata)占16字節(jié);也就是說,接口類型的對象由2個指針組成,與空接口(interface{})對象大小一樣。
比較函數(shù)
內(nèi)存數(shù)據(jù)顯示,接口類型的對象使用runtime.interequal進行相等性比較,該函數(shù)定義在runtime/alg.go源文件中:
- func interequal(p, q unsafe.Pointer) bool {
- x := *(*iface)(p)
- y := *(*iface)(q)
- return x.tab == y.tab && ifaceeq(x.tab, x.data, y.data)
- }
- func ifaceeq(tab *itab, x, y unsafe.Pointer) bool {
- if tab == nil {
- return true
- }
- t := tab._type
- eq := t.equal
- if eq == nil {
- panic(errorString("comparing uncomparable type " + t.string()))
- }
- if isDirectIface(t) {
- // See comment in efaceeq.
- return x == y
- }
- return eq(x, y)
- }
該函數(shù)的執(zhí)行邏輯是:
- 接口類型不同返回 false
- 接口類型為空返回 true
- 實現(xiàn)類型不可比較立即 panic
- 比較兩個實現(xiàn)類型的對象并返回結(jié)果
uncommonType
在接口類型數(shù)據(jù)中,包路徑信息可以通過interfaceType.pkgPath字段獲取,方法信息通過interfaceType.methods字段獲取, 因此uncommonType數(shù)據(jù)幾乎沒什么意義,只不過保持一致性罷了。
在本例中,可執(zhí)行程序.rodata節(jié)的起始地址是0x49a000, interfaceType.pkgPath=uncommonType.pkgPath+0x49a000。
接口方法
接口方法(reflect.imethod)只有名稱和類型信息,沒有可執(zhí)行指令,所以相對普通方法(reflect.method)缺少兩個字段。
foo接口的方法的名稱和類型,與fooImpl類型的方法的名稱和類型完全一致,此處不再贅述。如有需要請閱讀上文中方法相關(guān)的內(nèi)容。
接口對象
runtime.interequal函數(shù)源碼清晰地顯示,其比較的是兩個runtime.iface對象。
runtime.iface結(jié)構(gòu)體定義在runtime/runtime2.go源碼文件中,包含兩個指針字段,大小是16個字節(jié)(rtype.size)。
- type iface struct {
- tab *itab
- data unsafe.Pointer
- }
- type itab struct {
- inter *interfacetype // 接口類型
- _type *_type // 具體實現(xiàn)類型
- 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.
- }
該結(jié)構(gòu)體與reflect/value.go源文件中定義的nonEmptyInterface結(jié)構(gòu)體是等價的:
- type nonEmptyInterface struct {
- itab *struct {
- ityp *rtype // 接口類型
- typ *rtype // 具體實現(xiàn)類型
- hash uint32 // 實現(xiàn)類型哈希種子
- _ [4]byte // 內(nèi)存對齊
- fun [100000]unsafe.Pointer // 方法數(shù)組,編譯器控制數(shù)組長度
- }
- word unsafe.Pointer // 具體實現(xiàn)類型對象
- }
沒錯,接口對象就是iface對象,接口對象就是nonEmptyInterface對象。
源碼清單中的exec函數(shù)接受一個foo接口類型的參數(shù),在該函數(shù)入口處設(shè)置斷點,即可查看其參數(shù):
內(nèi)存數(shù)據(jù)顯示,exec函數(shù)的參數(shù)foo的值如下偽代碼所示:
- foo := runtime.iface {
- tab: 0x4dcbb8,
- data: 0x543ad8, // 指向整數(shù) 123
- }
iface.data指針指向的內(nèi)存數(shù)據(jù)是整數(shù)123,關(guān)于整數(shù)和runtime.staticuint64s,請閱讀【Go】內(nèi)存中的整數(shù)。
iface.tab指針指向一個全局符號go.itab.main.fooImpl,main.foo。該符號可以被視為一個全局常量,它是由Go編譯器生成的,保存在可執(zhí)行程序的.rodata節(jié),其值如下偽代碼所示:
- go.itab.main.fooImpl,main.foo = & runtime.itab {
- inter: 0x4aa5c0, // foo 接口類型的地址,上文已經(jīng)詳細分析
- _type: 0x4a9be0, // fooImpl 實現(xiàn)類型的地址,上文已經(jīng)詳細分析
- hash: 0xb597252a, // fooImpl 類型的哈希種子拷貝
- fun: [0x4999a0, 0x499a20, 0x499aa0] // 方法數(shù)組
- }
在本例中,runtime.iface.tab.fun字段值包含三個指針,分別指向以下三個函數(shù):
- main.(*fooImpl).Foo (0x4999a0)
- main.(*fooImpl).String (0x499a20)
- main.(*fooImpl).ree (0x499aa0)
當(dāng)exec函數(shù)調(diào)用foo接口的方法時,實際是從runtime.iface.tab.fun字段的數(shù)組中獲得方法地址;
所以,在本例中,exec`函數(shù)只能尋址以上三個方法,而無法尋址以下三個方法:
- main.fooImpl.Foo
- main.fooImpl.String
- main.fooImpl.ree
如果定義新的類型實現(xiàn)了foo接口,作為參數(shù)傳遞給exec函數(shù),Go編譯器就會生成新的runtime.itab對象,并命名為go.itab.${pkg}.${type},main.foo格式,也是以相同的方式進行調(diào)用和執(zhí)行。
在Go語言中,接口方法的調(diào)用邏輯是一致的。
接口擴展(繼承)
在源碼清單中,foo接口繼承了fmt.Stringer接口,并擴展了兩個方法。
- type foo interface {
- fmt.Stringer
- Foo()
- ree()
- }
而在程序運行時的內(nèi)存數(shù)據(jù)中,在動態(tài)調(diào)試過程中,根本就沒有fmt.Stringer接口什么事,連根毛都沒看見。
實際上,Go編譯器把foo接口的定義調(diào)整為以下代碼,這就是接口繼承和擴展的本質(zhì)。
- type foo interface {
- String() string
- Foo()
- ree()
- }
總結(jié)
本文完整地、詳細地、深入地剖析了Go語言接口的類型結(jié)構(gòu)、對象結(jié)構(gòu)、實現(xiàn)類型、方法調(diào)用、繼承擴展等等的各個方面的底層原理。
相信這是對Go接口類型的一次重新認識。