一文帶你了解Golang中interface的設計與實現
在上一篇文章《go interface 基本用法》中,我們了解了 go 中interface
的一些基本用法,其中提到過接口本質是一種自定義類型,本文就來詳細說說為什么說接口本質是一種自定義類型,以及這種自定義類型是如何構建起 go 的interface
系統(tǒng)的。
本文使用的源碼版本: go 1.19。另外本文中提到的 interface
和 接口
是同一個東西。
前言
在了解 go interface
的設計過程中,看了不少資料,但是大多數資料都有生成匯編的操作,但是在我的電腦上指向生成匯編的操作的時候, 生成的匯編代碼卻不太一樣,所以有很多的東西無法驗證正確性,這部分內容不會出現在本文中。本文只寫那些經過本機驗證正確的內容,但也不用擔心,因為涵蓋了 go interface
設計與實現的核心部分內容,但由于水平有限,所以只能盡可能地傳達我所知道的關于 interface
的一切東西。對于有疑問的部分,有興趣的讀者可以自行探索。
如果想詳細地了解,建議還是去看看 iface.go
,里面有接口實現的一些關鍵的細節(jié)。但是還是有一些東西被隱藏了起來, 導致我們無法知道我們 go 代碼會是 iface.go
里面的哪一段代碼實現的。
接口是什么
接口(interface
)本質上是一種結構體。
我們先來看看下面的代碼:
//?main.go package?main type?Flyable?interface?{ ?Fly() } //?go?tool?compile?-N?-S?-l?main.go func?main()?{ ?var?f1?interface{} ?println(f1)?//?CALL????runtime.printeface(SB) ?var?f2?Flyable ?println(f2)?//?CALL????runtime.printiface(SB) }
我們可以通過 go tool compile -N -S -l main.go
命令來生成 main.go
的偽匯編代碼,生成的代碼會很長,下面省略所有跟本文主題無關的代碼:
// main.go:10 => println(f1) 0x0029 00041 (main.go:10) CALL runtime.printeface(SB) // main.go:13 => println(f2) 0x004f 00079 (main.go:13) CALL runtime.printiface(SB)
我們從這段匯編代碼中可以看到,我們 println(f1)
實際上是對 runtime.printeface
的調用,我們看看這個 printeface
方法:
func?printeface(e?eface)?{ ?print("(",?e._type,?",",?e.data,?")") }
我們看到了,這個 printeface
接收的參數實際上是 eface
類型,而不是 interface{}
類型,我們再來看看 println(f2)
實際調用的 runtime.printiface
方法:
func?printiface(i?iface)?{ ?print("(",?i.tab,?",",?i.data,?")") }
也就是說 interface{}
類型在底層實際上是 eface
類型,而 Flyable
類型在底層實際上是 iface
類型。
這就是本文要講述的內容,go 中的接口變量其實是用 iface
和 eface
這兩個結構體來表示的:
iface
表示某一個具體的接口(含有方法的接口)。eface
表示一個空接口(interface{}
)
iface 和 eface 結構體
iface
和 eface
的結構體定義(runtime/iface.go
):
//?非空接口(如:io.Reader) type?iface?struct?{ ?tab??*itab??????????//?方法表 ?data?unsafe.Pointer?//?指向變量本身的指針 } //?空接口(interface{}) type?eface?struct?{ ?_type?*_type?????????//?接口變量的類型 ?data??unsafe.Pointer?//?指向變量本身的指針 }
go 底層的類型信息是使用 _type
結構體來存儲的。
比如,我們有下面的代碼:
package?main type?Bird?struct?{ ?name?string } func?(b?Bird)?Fly()?{ } type?Flyable?interface?{ ?Fly() } func?main()?{ ?bird?:=?Bird{name:?"b1"} ?var?efc?interface{}?=?bird?//?efc?是?eface ?var?ifc?Flyable?=?bird?//?ifc?是?iface ?println(efc)?//?runtime.printeface ?println(ifc)?//?runtime.printiface }
在上面代碼中,efc
是 eface
類型的變量,對應到 eface
結構體的話,_type
就是 Bird
這個類型本身,而 data
就是 &bird
這個指針:
類似的,ifc
是 iface
類型的變量,對應到 iface
結構體的話,data
也是 &bird
這個指針:
_type 是什么
在 go 中,_type
是保存了變量類型的元數據的結構體,定義如下:
//?_type?是?go?里面所有類型的一個抽象,里面包含?GC、反射、大小等需要的細節(jié), //?它也決定了?data?如何解釋和操作。 //?里面包含了非常多信息:類型的大小、哈希、對齊及?kind?等信息 type?_type?struct?{ ????size???????uintptr?//?數據類型共占用空間的大小 ????ptrdata????uintptr?//?含有所有指針類型前綴大小 ????hash???????uint32??//?類型?hash?值;避免在哈希表中計算 ????tflag??????tflag???//?額外類型信息標志 ????align??????uint8???//?該類型變量對齊方式 ????fieldAlign?uint8???//?該類型結構體字段對齊方式 ????kind???????uint8???//?類型編號 ????//?用于比較此類型對象的函數 ????equal?func(unsafe.Pointer,?unsafe.Pointer)?bool ????//?gc?相關數據 ????gcdata????*byte ????str???????nameOff?//?類型名字的偏移 ????ptrToThis?typeOff }
這個 _type
結構體定義大家隨便看看就好了,實際上,go 底層的類型表示也不是上面這個結構體這么簡單。
但是,我們需要知道的一點是(與本文有關的信息),通過 _type
我們可以得到結構體里面所包含的方法這些信息。具體我們可以看 itab
的 init
方法(runtime/iface.go
),我們會看到如下幾行:
typ?:=?m._type x?:=?typ.uncommon()?//?結構體類型 nt?:=?int(x.mcount)???//?實際類型的方法數量 //?實際類型的方法數組,數組元素為?method xmhdr?:=?(*[1?<<?16]method)(add(unsafe.Pointer(x),?uintptr(x.moff)))[:nt:nt]
在底層,go 是通過 _type
里面 uncommon
返回的地址,加上一個偏移量(x.moff
)來得到實際結構體類型的方法列表的。
我們可以參考一下下圖想象一下:
itab 是什么
我們從 iface
中可以看到,它包含了一個 *itab
類型的字段,我們看看這個 itab
的定義:
//?編譯器已知的?itab?布局 type?itab?struct?{ ?inter?*interfacetype?//?接口類型 ?_type?*_type ?hash??uint32 ?_?????[4]byte ?fun???[1]uintptr?//?變長數組.?fun[0]==0?意味著?_type?沒有實現?inter?這個接口 } //?接口類型 //?對應源代碼:type?xx?interface?{} type?interfacetype?struct?{ ????typ?????_type?????//?類型信息 ????pkgpath?name??????//?包路徑 ????mhdr????[]imethod?//?接口的方法列表 }
根據 interfacetype
我們可以得到關于接口所有方法的信息。同樣的,通過 _type
也可以獲取結構體類型的所有方法信息。
從定義上,我們可以看到 itab
跟 *interfacetype
和 *_type
有關,但實際上有什么關系從定義上其實不太能看得出來, 但是我們可以看它是怎么被使用的,現在,假設我們有如下代碼:
//?i?在底層是一個?interfacetype?類型 type?i?interface?{ ?A() ?C() } //?t?底層會用?_type?來表示 //?t?里面有?A、B、C、D?方法 //?因為實現了?i?中的所有方法,所以?t?實現了接口?i type?t?struct?{} func?(t)?A()??{} func?(t)?B()??{} func?(t)?C()??{} func?(t)?D()??{}
下圖描述了上面代碼對應的 itab
生成的過程:
說明:
itab
里面的inter
是接口類型的指針(比如通過type Reader interface{}
這種形式定義的接口,記錄的是這個類型本身的信息),這個接口類型本身定義了一系列的方法,如圖中的i
包含了A
、C
兩個方法。_type
是實際類型的指針,記錄的是這個實際類型本身的信息,比如這個類型包含哪些方法。圖中的i
實現了A
、B
、C
、D
四個方法,因為實現了i
的所有方法,所以說t
實現了i
接口。- 在底層做類型轉換的時候,比如
t
轉換為i
的時候(var v i = t{}
),會生成一個itab
,如果t
沒有實現i
中的所有方法,那么生成的itab
中不包含任何方法。 - 如果
t
實現了i
中的所有方法,那么生成的itab
中包含了i
中的所有方法指針,但是實際指向的方法是實際類型的方法(也就是指向的是t
中的方法地址) mhdr
就是itab
中的方法表,里面的方法名就是接口的所有方法名,這個方法表中保存了實際類型(t
)中同名方法的函數地址,通過這個地址就可以調用實際類型的方法了。
所以,我們有如下結論:
itab
實際上定義了interfacetype
和_type
之間方法的交集。作用是什么呢?就是用來判斷一個結構體是否實現某個接口的。itab
包含了接口的所有方法,這里面的方法是實際類型的子集。itab
里面的方法列表包含了實際類型的方法指針(也就是實際類型的方法的地址),通過這個地址可以對實際類型進行方法的調用。itab
在實際類型沒有實現接口的所有方法的時候,生成失?。ㄊ〉囊馑际?,生成的itab
里面的方法列表是空的,在底層實現上是用fun[0] = 0
來表示)。
生成的 itab 是怎么被使用的
go 里面定義了一個全局變量 itabTable
,用來緩存 itab
,因為在判斷某一個結構體是否實現了某一個接口的時候, 需要比較兩者的方法集,如果結構體實現了接口的所有方法,那么就表明結構體實現了接口(這也就是生成 itab
的過程)。 如果在每一次做接口斷言的時候都要做一遍這個比較,性能無疑會大大地降低,因此 go 就把這個比較得出的結果緩存起來,也就是 itab
。 這樣在下一次判斷結構體是否實現了某一個接口的時候,就可以直接使用之前的 itab
,性能也就得到提升了。
//?表里面緩存了?itab itabTable?????=?&itabTableInit itabTableInit?=?itabTableType{size:?itabInitSize} //?全局的?itab?表 type?itabTableType?struct?{ ????size????uintptr?????????????//?entries?的長度,2?的次方 ????count???uintptr?????????????//?當前?entries?的數量 ????entries?[itabInitSize]*itab?//?保存?itab?的哈希表 }
itabTableType
里面的 entries
是一個哈希表,在實際保存的時候,會用 interfacetype
和 _type
這兩個生成一個哈希表的鍵。 也就是說,這個保存 itab
的緩存哈希表中,只要我們有 interfacetype
和 _type
這兩個信息,就可以獲取一個 itab
。
具體怎么使用,我們可以看看下面的例子:
package?main type?Flyable?interface?{ ?Fly() } type?Runnable?interface?{ ?Run() } var?_?Flyable?=?(*Bird)(nil) var?_?Runnable?=?(*Bird)(nil) type?Bird?struct?{ } func?(b?Bird)?Fly()?{ } func?(b?Bird)?Run()?{ } //?GOOS=linux?GOARCH=amd64?go?tool?compile?-N?-S?-l?main.go?>?main.s func?test()?{ ?//?f?的類型是?iface ?var?f?Flyable?=?Bird{} ?//?Flyable?轉?Runnable?本質上是?iface?到?iface?的轉換 ?f.(Runnable).Run()?//?CALL?runtime.assertI2I(SB) ?//?這個?switch?里面的類型斷言本質上也是?iface?到?iface?的轉換 ?//?但是?switch?里面的類型斷言失敗不會引發(fā)?panic ?switch?f.(type)?{ ?case?Flyable:?//?CALL?runtime.assertI2I2(SB) ?case?Runnable:?//?CALL?runtime.assertI2I2(SB) ?} ?if?_,?ok?:=?f.(Runnable);?ok?{?//?CALL?runtime.assertI2I2(SB) ?} ?//?i?的類型是?eface ?var?i?interface{}?=?Bird{} ?//?i?轉?Flyable?本質上是?eface?到?iface?的轉換 ?i.(Flyable).Fly()?//?CALL?runtime.assertE2I(SB) ?//?這個?switch?里面的類型斷言本質上也是?eface?到?iface?的轉換 ?//?但是?switch?里面的類型斷言失敗不會引發(fā)?panic ?switch?i.(type)?{ ?case?Flyable:?//?CALL?runtime.assertE2I2(SB) ?case?Runnable:?//?CALL?runtime.assertE2I2(SB) ?} ?if?_,?ok?:=?i.(Runnable);?ok?{?//?CALL?runtime.assertE2I2(SB) ?} }
我們對上面的代碼生成偽匯編代碼:
GOOS=linux GOARCH=amd64 go tool compile -N -S -l main.go > main.s
然后我們去查看 main.s
,就會發(fā)現類型斷言的代碼,本質上是對 runtime.assert*
方法的調用(assertI2I
、assertI2I2
、assertE2I
、assertE2I2
), 這幾個方法名都是以 assert
開頭的,assert
在編程語言中的含義是,判斷后面的條件是否為 true
,如果 false
則拋出異?;蛘咂渌袛喑绦驁?zhí)行的操作,為 true
則接著執(zhí)行。 這里的用處就是,判斷一個接口是否能夠轉換為另一個接口或者另一個類型。
但在這里有點不太一樣,這里有兩個函數最后有個數字 2
的,表明了我們對接口的類型轉換會有兩種情況,我們上面的代碼生成的匯編其實已經很清楚了,一種情況是直接斷言,使用 i.(T)
這種形式,另外一種是在 switch...case
里面使用,。
我們可以看看它們的源碼,看看有什么不一樣:
//?直接根據?interfacetype/_type?獲取?itab func?assertE2I(inter?*interfacetype,?t?*_type)?*itab?{ ?if?t?==?nil?{ ??//?顯式轉換需要非nil接口值。 ??panic(&TypeAssertionError{nil,?nil,?&inter.typ,?""}) ?} ?//?getitab?的第三個參數是?false ?//?表示?getiab?獲取不到?itab?的時候需要?panic ?return?getitab(inter,?t,?false) } //?將?eface?轉換為?iface //?因為?e?包含了?*_type func?assertE2I2(inter?*interfacetype,?e?eface)?(r?iface)?{ ?t?:=?e._type ?if?t?==?nil?{ ??return ?} ?//?getitab?的第三個參數是?true ?//?表示?getitab?獲取不到?itab?的時候不需要?panic ?tab?:=?getitab(inter,?t,?true) ?if?tab?==?nil?{ ??return ?} ?r.tab?=?tab ?r.data?=?e.data ?return }
getitab
的源碼后面會有。
從上面的代碼可以看到,其實帶 2
和不帶 2
后綴的關鍵區(qū)別在于:getitab
的調用允不允許失敗。 這有點類似于 chan
里面的 select
,chan
的 select
語句中讀寫 chan
不會阻塞,而其他地方會阻塞。
assertE2I2
是用在 switch...case
中的,這個調用是允許失敗的,因為我們還需要判斷能否轉換為其他類型; 又或者 v, ok := i.(T)
的時候,也是允許失敗的,但是這種情況會返回第二個值給用戶判斷是否轉換成功。 而直接使用類型斷言的時候,如 i.(T)
這種,如果 i
不能轉換為 T
類型,則直接 panic
。
對于 go 中的接口斷言可以總結如下:
assertI2I
用于將一個iface
轉換為另一個iface
zhong,轉換失敗的時候會panic
assertI2I2
用于將一個iface
轉換為另一個iface
,轉換失敗的時候不會panic
assertE2I
用于將一個eface
轉換為另一個iface
,轉換失敗的時候會panic
assertE2I2
用于將一個eface
轉換為另一個iface
,轉換失敗的時候不會panic
assert
相關的方法后綴的I2I
、E2E
里面的I
表示的是iface
,E
表示的是eface
- 帶
2
后綴的允許失敗,用于v, ok := i.(T)
或者switch x.(type) ... case
中 - 不帶
2
后綴的不允許失敗,用于i.(T)
這種形式中
當然,這里說的轉換不是說直接轉換,只是說,在轉換的過程中會用到 assert* 方法。
如果我們足夠細心,然后也去看了 assertI2I
和 assertI2I2
的源碼,就會發(fā)現,這幾個方法本質上都是, 通過 interfacetype
和 _type
來獲取一個 itab
然后轉換為另外一個 itab
或者 `iface。
同時,我們也應該注意到,上面的轉換都是轉換到 iface 而沒有轉換到 eface 的操作,這是因為,所有類型都可以轉換為空接口(interface{},也就是 eface)。根本就不需要斷言。
上面的內容可以結合下圖理解一下:
itab 關鍵方法的實現
下面,讓我們再來深入了解一下 itab
是怎么被創(chuàng)建出來的,以及是怎么保存到全局的哈希表中的。我們先來看看下圖:
這個圖描述了 go 底層存儲 itab
的方式:
- 通過一個
itabTableType
類型來存儲所有的itab
。 - 在調用
getitab
的時候,會先根據inter
和_type
計算出哈希值,然后從entries
中查找是否存在,存在就返回對應的itab
,不存在則新建一個itab
。 - 在調用
itabAdd
的時候,會將itab
加入到itabTableType
類型變量里面的entries
中,其中entries
里面的鍵是根據inter
和_type
做哈希運算得出的。
itab
兩個比較關鍵的方法:
getitab
讓我們可以通過interfacetype
和_type
獲取一個itab
,會現在緩存中找,找不到會新建一個。itabAdd
是在我們緩存找不到itab
,然后新建之后,將這個新建的itab
加入到緩存的方法。
getitab
方法的第三個參數 canfail
表示當前操作是否允許失敗,上面說了,如果是用在 switch...case
或者 v, ok := i.(T)
這種是允許失敗的。
//?獲取某一個類型的?itab(從?itabTable?中查找,鍵是?inter?和?_type?的哈希值) //?查找?interfacetype?+?_type?對應的?itab //?找不到就新增。 func?getitab(inter?*interfacetype,?typ?*_type,?canfail?bool)?*itab?{ ?if?len(inter.mhdr)?==?0?{ ??throw("internal?error?-?misuse?of?itab") ?} ?//?不包含?Uncommon?信息的類型直接報錯 ?if?typ.tflag&tflagUncommon?==?0?{ ??if?canfail?{ ???return?nil ??} ??name?:=?inter.typ.nameOff(inter.mhdr[0].name) ??panic(&TypeAssertionError{nil,?typ,?&inter.typ,?name.name()}) ?} ?//?保存返回的?itab ?var?m?*itab ?//?t?指向了?itabTable(全局的?itab?表) ?t?:=?(*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable))) ?//?會先從全局?itab?表中查找,找到就直接返回 ?if?m?=?t.find(inter,?typ);?m?!=?nil?{ ??goto?finish ?} ?//?沒有找到,獲取鎖,再次查找。 ?//?找到則返回 ?lock(&itabLock) ?if?m?=?itabTable.find(inter,?typ);?m?!=?nil?{ ??unlock(&itabLock) ??goto?finish ?} ?//?沒有在緩存中找到,新建一個?itab ?m?=?(*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*goarch.PtrSize,?0,?&memstats.other_sys)) ?//?itab?的 ?m.inter?=?inter ?m._type?=?typ ?m.hash?=?0 ?//?itab?初始化 ?m.init() ?//?將新創(chuàng)建的?itab?加入到全局的?itabTable?中 ?itabAdd(m) ?//?釋放鎖 ?unlock(&itabLock) finish: ?//?==?0?表示沒有任何方法 ?//?下面?!=?0?表示有?inter?和?typ?有方法的交集 ?if?m.fun[0]?!=?0?{ ??return?m ?} ?//?用在?switch?x.(type)?中的時候,允許失敗而不是直接?panic ?//?但在?x.(Flyable).Fly()?這種場景會直接?panic ?if?canfail?{ ??return?nil ?} ?//?沒有找到有方法的交集,panic ?panic(&TypeAssertionError{concrete:?typ,?asserted:?&inter.typ,?missingMethod:?m.init()}) }
itabAdd
將給定的 itab
添加到 itab
哈希表中(itabTable
)。
注意:itabAdd
中在判斷到哈希表的使用量超過 75%
的時候,會進行擴容,新的容量為舊容量的 2 倍。
//?必須保持?itabLock。 func?itabAdd(m?*itab)?{ ?//?正在分配內存的時候調用的話報錯 ?if?getg().m.mallocing?!=?0?{ ??throw("malloc?deadlock") ?} ?t?:=?itabTable ?//?容量已經超過?75%?的負載了,hash?表擴容 ?if?t.count?>=?3*(t.size/4)?{ ??//?75%?load?factor(實際上是:t.size?*0.75) ??//?擴展哈希表。原來?2?倍大小。 ??//?我們撒謊告訴?malloc?我們需要無指針內存,因為所有指向的值都不在堆中。 ??//?2?是?size?和?count?這兩個字段需要的空間 ??t2?:=?(*itabTableType)(mallocgc((2+2*t.size)*goarch.PtrSize,?nil,?true)) ??t2.size?=?t.size?*?2 ??//?復制條目。 ??//?注意:在復制時,其他線程可能會查找itab,但找不到它。 ??//?沒關系,然后它們會嘗試獲取itab鎖,因此等待復制完成。 ??iterate_itabs(t2.add)????//?遍歷舊的?hash?表,復制函數指針到?t2?中 ??if?t2.count?!=?t.count?{?//?復制出錯 ???throw("mismatched?count?during?itab?table?copy") ??} ??//?發(fā)布新哈希表。使用原子寫入:請參見?getitab?中的注釋。 ??//?使用?t2?覆蓋?itabTable ??atomicstorep(unsafe.Pointer(&itabTable),?unsafe.Pointer(t2)) ??//?使用新的?hash?表 ??//?因為?t?是局部變量,指向舊的地址, ??//?但是擴容之后是新的地址了,所以現在需要將新的地址賦給?t ??t?=?itabTable ??//?注:舊的哈希表可以在此處進行GC。 ?} ?//?將?itab?加入到全局哈希表 ?t.add(m) }
其實 itabAdd
的關鍵路徑比較清晰,只是因為它是一個哈希表,所以里面在判斷到當前 itab
的數量超過 itabTable
容量的 75%
的時候,會對 itabTable
進行 2 倍擴容。
根據 interfacetype 和 _type 初始化 itab
上面那個圖我們說過,itab
本質上是 interfacetype
和 _type
方法的交集,這一節(jié)我們就來看看,itab
是怎么根據這兩個類型來進行初始化的。
itab
的 init
方法實現:
//?init?用?m.inter/m._type?對的所有代碼指針填充?m.fun?數組。 //?如果該類型不實現接口,它將?m.fun[0]?設置為?0?,并返回缺少的接口函數的名稱。 //?可以在同一個m上多次調用,甚至同時調用。 func?(m?*itab)?init()?string?{ ?inter?:=?m.inter????//?接口 ?typ?:=?m._type??????//?實際的類型 ?x?:=?typ.uncommon() ?//?inter?和?typ?都具有按名稱排序的方法,并且接口名稱是唯一的,因此可以在鎖定步驟中迭代這兩個; ?//?循環(huán)時間復雜度是?O(ni+nt),不是?O(ni*nt) ?ni?:=?len(inter.mhdr)?//?接口的方法數量 ?nt?:=?int(x.mcount)???//?實際類型的方法數量 ?//?實際類型的方法數組,數組元素為?method ?xmhdr?:=?(*[1?<<?16]method)(add(unsafe.Pointer(x),?uintptr(x.moff)))[:nt:nt]?//?大小無關緊要,因為下面的指針訪問不會超出范圍 ?j?:=?0 ?//?用來保存?inter/_type?對方法列表的數組,數組元素為?unsafe.Pointer(是實際類型方法的指針) ?methods?:=?(*[1?<<?16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]?//?保存?itab?方法的數組 ?//?第一個方法的指針 ?var?fun0?unsafe.Pointer imethods: ?for?k?:=?0;?k?<?ni;?k++?{?//?接口方法遍歷 ??i?:=?&inter.mhdr[k]????????????????//?i?是接口方法,?imethod?類型 ??itype?:=?inter.typ.typeOff(i.ityp)?//?接口的方法類型 ??name?:=?inter.typ.nameOff(i.name)??//?接口的方法名稱 ??iname?:=?name.name()???????????????//?接口的方法名 ??ipkg?:=?name.pkgPath()?????????????//?接口的包路徑 ??if?ipkg?==?""?{ ???ipkg?=?inter.pkgpath.name() ??} ??//?根據接口方法查找實際類型的方法 ??for?;?j?<?nt;?j++?{?//?實際類型的方法遍歷 ???t?:=?&xmhdr[j]???????????????//?t?是實際類型的方法,method?類型 ???tname?:=?typ.nameOff(t.name)?//?實際類型的方法名 ???//?比較接口的方法跟實際類型的方法是否一致 ???if?typ.typeOff(t.mtyp)?==?itype?&&?tname.name()?==?iname?{ ????//?實際類型的包路徑 ????pkgPath?:=?tname.pkgPath() ????if?pkgPath?==?""?{ ?????pkgPath?=?typ.nameOff(x.pkgpath).name() ????} ????//?如果是導出的方法 ????//?則保存到?itab?中 ????if?tname.isExported()?||?pkgPath?==?ipkg?{ ?????if?m?!=?nil?{ ??????ifn?:=?typ.textOff(t.ifn)?//?實際類型的方法指針(通過這個指針可以調用實際類型的方法) ??????if?k?==?0?{ ???????//?第一個方法 ???????fun0?=?ifn?//?we'll?set?m.fun[0]?at?the?end ??????}?else?{ ???????methods[k]?=?ifn ??????} ?????} ?????//?比較下一個方法 ?????continue?imethods ????} ???} ??} ??//?沒有實現接口(實際類型沒有實現?interface?中的任何一個方法) ??m.fun[0]?=?0 ??return?iname?//?返回缺失的方法名,返回值在類型斷言失敗的時候會需要提示用戶 ?} ?//?實現了接口 ?m.fun[0]?=?uintptr(fun0) ?return?"" }
接口斷言過程總覽(類型轉換的關鍵)
具體來說有四種情況,對應上面提到的 runtime.assert*
方法:
- 實際類型轉換到
iface
iface
轉換到另一個iface
- 實際類型轉換到
eface
eface
轉換到iface
這其中的關鍵是 interfacetype
+ _type
可以生成一個 itab
。
上面的內容可能有點混亂,讓人摸不著頭腦,但是我們通過上面的講述,相信已經了解了 go 接口中底層的一些實現細節(jié),現在,就讓我們重新來捋一下,看看 go 接口到底是怎么實現的:
首先,希望我們可以達成的一個共識就是,go 的接口斷言本質上是類型轉換,switch...case
里面或 v, ok := i.(T)
允許轉換失敗,而 i.(T).xx()
這種不允許轉換失敗,轉換失敗的時候會 panic
。
接著,我們就可以通過下圖來了解 go 里面的接口整體的實現原理了(還是以上面的代碼作為例子):
1.將結構體賦值給接口類型:var f Flyable = Bird{}
在這個賦值過程中,創(chuàng)建了一個 iface
類型的變量,這個變量中的 itab
的方法表只包含了 Flyable
定義的方法。
2.iface
轉另一個 iface
:
f.(Runnable)
_, ok := f.(Runnable)
switch f.(type)
里面的case
是Runnable
在這個斷言過程中,會將 Flyable
轉換為 Runnable
,本質上是一個 iface
轉換到另一個 iface
。但是有個不同之處在于, 兩個 iface
里面的方法列表是不一樣的,只包含了當前 interfacetype
里面定義的方法。
3.將結構體賦值給空接口:var i interface{} = Bird{}
在這個過程中,創(chuàng)建了一個 eface
類型的變量,這個 eface
里面只包含了類型信息以及實際的 Bird
結構體實例。
4.eface
轉換到 iface
i.(Flyable)
_, ok := i.(Runnable)
switch i.(type)
里面的case
是Flyable
因為 _type
包含了 Bird
類型的所有信息,而 data
包含了 Bird
實例的值,所以這個轉換是可行的。
panicdottypeI 與 panicdottypeE
從前面的幾個小節(jié),我們知道,go 的 iface
類型轉換使用的是 runtime.assert*
幾個方法,還有另外一種情況就是, 在編譯期間編譯器就已經知道了無法轉換成功的情況,比如下面的代碼:
package?main type?Flyable?interface?{ ?Fly() } type?Cat?struct?{ } func?(c?Cat)?Fly()?{ } func?(c?Cat)?test()?{ } //?GOOS=linux?GOARCH=amd64?go?tool?compile?-N?-S?-l?main.go?>?main.s func?main()?{ ?var?b?interface{} ?var?_?=?b.(int)?//?CALL?runtime.panicdottypeE(SB) ?var?c?Flyable?=?&Cat{} ?c.(Cat).test()?//?CALL?runtime.panicdottypeI(SB) }
上面的兩個轉換都是錯誤的,第一個 b.(int)
嘗試將 nil
轉換為 int
類型,第二個嘗試將 *Cat
類型轉換為 Cat
類型, 這兩個錯誤的類型轉換都在編譯期可以發(fā)現,因此它們生成的匯編代碼調用的是 runtime.panicdottypeE
和 runtime.panicdottypeI
方法:
//?在執(zhí)行?e.(T)?轉換時如果轉換失敗,則調用?panicdottypeE //?have:我們的動態(tài)類型。 //?want:我們試圖轉換為的靜態(tài)類型。 //?iface:我們正在轉換的靜態(tài)類型。 //?轉換的過程:嘗試將?iface?的?have?轉換為?want?失敗了。 //?不是調用方法的時候的失敗。 func?panicdottypeE(have,?want,?iface?*_type)?{ ?panic(&TypeAssertionError{iface,?have,?want,?""}) } //?當執(zhí)行?i.(T)?轉換并且轉換失敗時,調用?panicdottypeI //?跟?panicdottypeE?參數相同,但是?hava?是動態(tài)的?itab?類型 func?panicdottypeI(have?*itab,?want,?iface?*_type)?{ ?var?t?*_type ?if?have?!=?nil?{ ??t?=?have._type ?} ?panicdottypeE(t,?want,?iface) }
這兩個方法都是引發(fā)一個 panic
,因為我們的類型轉換失敗了:
iface 和 eface 里面的 data 是怎么來的
我們先看看下面的代碼:
package?main type?Bird?struct?{ } func?(b?Bird)?Fly()?{ } type?Flyable?interface?{ ?Fly() } //?GOOS=linux?GOARCH=amd64?go?tool?compile?-N?-S?-l?main.go?>?main.s func?main()?{ ?bird?:=?Bird{} ?var?efc?interface{}?=?bird?//?CALL?runtime.convT(SB) ?var?ifc?Flyable?=?bird?????//?CALL?runtime.convT(SB) ?println(efc,?ifc) }
我們生成偽匯編代碼發(fā)現,里面將結構體變量賦值給接口類型變量的時候,實際上是調用了 convT
方法。
convT* 方法
iface
里面還包含了幾個 conv*
前綴的函數,在我們將某一具體類型的值賦值給接口類型的時候,go 底層會將具體類型的值通過 conv*
函數轉換為 iface
里面的 data
指針:
//?convT?將?v?指向的?t?類型的值轉換為可以用作接口值的第二個字的指針(接口的第二個字是指向?data?的指針)。 //?data(Pointer)?=>?指向?interface?第?2?個字的?Pointer func?convT(t?*_type,?v?unsafe.Pointer)?unsafe.Pointer?{ ?//?...?其他代碼 ?//?分配?_type?類型所需要的內存 ?x?:=?mallocgc(t.size,?t,?true) ?//?將?v?指向的值復制到剛剛分配的內存上 ?typedmemmove(t,?x,?v) ?return?x }
我們發(fā)現,在這個過程,實際上是將值復制了一份:
iface.go
里面還有將無符號值轉換為 data
指針的函數,但是還不知道在什么地方會用到這些方法,如:
//?轉換?uint16?類型值為?interface?里面?data?的指針。 //?如果是?0~255?的整數,返回指向?staticuint64s?數組里面對應下標的指針。 //?否則,分配新的內存地址。 func?convT16(val?uint16)?(x?unsafe.Pointer)?{ ?//?如果小于?256,則使用共享的內存地址 ?if?val?<?uint16(len(staticuint64s))?{ ??x?=?unsafe.Pointer(&staticuint64s[val]) ??if?goarch.BigEndian?{ ???x?=?add(x,?6) ??} ?}?else?{ ??//?否則,分配新的內存 ??x?=?mallocgc(2,?uint16Type,?false) ??*(*uint16)(x)?=?val ?} ?return }
個人猜測,僅僅代表個人猜測,在整數賦值給 iface
或者 eface
的時候會調用這類方法。不管調不調用,我們依然可以看看它的設計,因為有些值得學習的地方:
staticuint64s
是一個全局整型數組,里面存儲的是 0~255
的整數。上面的代碼可以表示為下圖:
這個函數跟上面的 convT
的不同之處在于,它在判斷整數如果小于 256
的時候,則使用的是 staticuint64s
數組里面對應下標的地址。 為什么這樣做呢?本質上是為了節(jié)省內存,因為對于數字來說,其實除了值本身,沒有包含其他的信息了,所以如果對于每一個整數都分配新的內存來保存, 無疑會造成浪費。按 convT16
里面的實現方式,對于 0~255
之間的整數,如果需要給它們分配內存,就可以使用同一個指針(指向 staticuint64s[]
數組中元素的地址)。
這實際上是享元模式。
Java 里面的小整數享元模式
go 里使用 staticuint64s
的方式,其實在 Java 里面也有類似的實現,Java 中對于小整數也是使用了享元模式, 這樣在裝箱的時候,就不用分配新的內存了,就可以使用共享的一塊內存了,當然,某一個整數能節(jié)省的內存非常有限,如果需要分配內存的小整數非常大,那么節(jié)省下來的內存就非??陀^了。 當然,也不只是能節(jié)省內存這唯一的優(yōu)點,從另一方面說,它也節(jié)省了垃圾回收器回收內存的開銷,因為不需要管理那么多內存。
我們來看看 Java 中的例子:
class?Test?{ ????public?static?void?main(String[]?args)?{ ????????Integer?k1?=?127; ????????Integer?k2?=?127; ????????System.out.println(k1?==?k2);?//?true ????????System.out.println(k1.equals(k2));?//?true ????????Integer?k10?=?128; ????????Integer?k20?=?128; ????????System.out.println(k10?==?k20);?//?false ????????System.out.println(k10.equals(k20));?//?true ????} }
Java 里面有點不一樣,它是對 -128~127
范圍內的整數做了享元模式的處理,而 go 里面是 0~255
。
上面的代碼中,當我們使用 ==
來比較 Integer
的時候,值相等的兩個數,在 -128~127
的范圍的時候,返回的是 true
,超出這個范圍的時候比較返回的是 false
。 這是因為在 -128~127
的時候,值相等的兩個數字指向了相同的內存地址,超出這個范圍的時候,值相等的兩個數指向了不同的地址。
Java 的詳細實現可以看 java.lang.Integer.IntegerCache
。
總結
- go 的的接口(
interface
)本質上是一種結構體,底冊實現是iface
和eface
,iface
表示我們通過type i interface{}
定義的接口,而eface
表示interface{}/any
,也就是空接口。 iface
里面保存的itab
中保存了具體類型的方法指針列表,data
保存了具體類型值的內存地址。eface
里面保存的_type
包含了具體類型的所有信息,data
保存了具體類型值的內存地址。itab
是底層保存接口類型跟具體類型方法交集的結構體,如果具體類型實現了接口的所有方法,那么這個itab
里面的保存有指向具體類型方法的指針。如果具體類型沒有實現接口的全部方法,那么itab
中的不會保存任何方法的指針(從itab
的作用上看,我們可以看作是一個空的itab
)。- 不管
itab
的方法列表是否為空,interfacetype
和_type
比較之后生成的itab
會緩存下來,在后續(xù)比較的時候可以直接使用緩存。 _type
是 go 底層用來表示某一個類型的結構體,包含了類型所需空間大小等信息。- 類型斷言
i.(T)
本質上是iface
到iface
的轉換,或者是eface
到iface
的轉換,如果沒有第二個返回值,那么轉換失敗的時候會引發(fā)panic
。 switch i.(type) { case ...}
本質上也是iface
或eface
到iface
的轉換,但是轉換失敗的時候不會引發(fā)panic
。- 全局的保存
itab
的緩存結構體,底層是使用了一個哈希表來保存itab
的,在哈希表使用超過75%
的時候,會觸發(fā)擴容,新的哈希表容量為舊的2
倍。 staticuint64s
使用了享元模式,Java 中也有類似的實現。
以上就是一文帶你了解Golang中interface的設計與實現的詳細內容,更多關于Golang interface的資料請關注腳本之家其它相關文章!
相關文章
Go語言服務器開發(fā)實現最簡單HTTP的GET與POST接口
這篇文章主要介紹了Go語言服務器開發(fā)實現最簡單HTTP的GET與POST接口,實例分析了Go語言http包的使用技巧,需要的朋友可以參考下2015-02-02Go語言網站使用異步編程和Goroutine提高Web的性能
作為一門現代化編程語言,Go語言提供了強大的異步編程能力,使得程序員可以以更高效的方式處理并發(fā)任務,在Go語言中,使用Goroutine在單個進程中實現多任務并行處理,以及如何使用協程池來進一步提高Web服務器的處理能力,2024-01-01Golang常見錯誤之值拷貝和for循環(huán)中的單一變量詳解
這篇文章主要給大家介紹了關于Golang常見錯誤之值拷貝和for循環(huán)中單一變量的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2017-11-11