欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你了解Golang中interface的設計與實現

 更新時間:2023年01月04日 11:28:49   作者:rubys_  
本文就來詳細說說為什么說?接口本質是一種自定義類型,以及這種自定義類型是如何構建起?go?的?interface?系統(tǒng)的,感興趣的小伙伴可以跟隨小編一起學習一下

在上一篇文章《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 中的接口變量其實是用 ifaceeface 這兩個結構體來表示的:

  • iface 表示某一個具體的接口(含有方法的接口)。
  • eface 表示一個空接口(interface{}

iface 和 eface 結構體

ifaceeface 的結構體定義(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
}

在上面代碼中,efceface 類型的變量,對應到 eface 結構體的話,_type 就是 Bird 這個類型本身,而 data 就是 &bird 這個指針:

類似的,ifciface 類型的變量,對應到 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 我們可以得到結構體里面所包含的方法這些信息。具體我們可以看 itabinit 方法(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 包含了 AC 兩個方法。
  • _type 是實際類型的指針,記錄的是這個實際類型本身的信息,比如這個類型包含哪些方法。圖中的 i 實現了 A、BC、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、assertE2IassertE2I2), 這幾個方法名都是以 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,chanselect 語句中讀寫 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* 方法。

如果我們足夠細心,然后也去看了 assertI2IassertI2I2 的源碼,就會發(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 是怎么根據這兩個類型來進行初始化的。

itabinit 方法實現:

//?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) 里面的 caseRunnable

在這個斷言過程中,會將 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) 里面的 caseFlyable

因為 _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.panicdottypeEruntime.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)本質上是一種結構體,底冊實現是 ifaceeface,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) 本質上是 ifaceiface 的轉換,或者是 efaceiface 的轉換,如果沒有第二個返回值,那么轉換失敗的時候會引發(fā) panic。
  • switch i.(type) { case ...} 本質上也是 ifaceefaceiface 的轉換,但是轉換失敗的時候不會引發(fā) panic。
  • 全局的保存 itab 的緩存結構體,底層是使用了一個哈希表來保存 itab 的,在哈希表使用超過 75% 的時候,會觸發(fā)擴容,新的哈希表容量為舊的 2 倍。
  • staticuint64s 使用了享元模式,Java 中也有類似的實現。

以上就是一文帶你了解Golang中interface的設計與實現的詳細內容,更多關于Golang interface的資料請關注腳本之家其它相關文章!

相關文章

  • 詳解go如何優(yōu)雅的使用接口與繼承

    詳解go如何優(yōu)雅的使用接口與繼承

    Go語言中的接口和嵌套結構體是兩種重要的代碼設計方式,接口定義了一組方法簽名,使得不同的類型能夠以相同的方式進行交互,本文將給大家介紹go語言如何優(yōu)雅的使用接口與繼承,文中有詳細的代碼供大家參考,需要的朋友可以參考下
    2024-06-06
  • golang動態(tài)創(chuàng)建類的示例代碼

    golang動態(tài)創(chuàng)建類的示例代碼

    這篇文章主要介紹了golang動態(tài)創(chuàng)建類的實例代碼,本文通過實例代碼給大家講解的非常詳細,需要的朋友可以參考下
    2023-06-06
  • Go語言安裝和GoLand2021最全超詳細安裝教程

    Go語言安裝和GoLand2021最全超詳細安裝教程

    Go語言和GoLand的關系好比于java和idea、python和pycharm,因此我們需要先安裝好Go語言后才能安裝GoLand。它的安裝和java,python的安裝大同小異,好了,下面給大家?guī)砹薌oLand2021安裝教程,需要的朋友參考下吧
    2021-08-08
  • go語言算法題解二叉樹的拷貝、鏡像和對稱

    go語言算法題解二叉樹的拷貝、鏡像和對稱

    這篇文章主要為大家詳細介紹了go語言算法題解二叉樹的拷貝、鏡像和對稱,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下
    2023-01-01
  • Golang?中實現?Set的思路詳解

    Golang?中實現?Set的思路詳解

    本文介紹了Go中兩種set的實現原理,并在此基礎介紹了對應于它們的兩個包簡單使用,本文介紹的非常詳細,需要的朋友參考下吧
    2024-01-01
  • Go語言服務器開發(fā)實現最簡單HTTP的GET與POST接口

    Go語言服務器開發(fā)實現最簡單HTTP的GET與POST接口

    這篇文章主要介紹了Go語言服務器開發(fā)實現最簡單HTTP的GET與POST接口,實例分析了Go語言http包的使用技巧,需要的朋友可以參考下
    2015-02-02
  • Go語言網站使用異步編程和Goroutine提高Web的性能

    Go語言網站使用異步編程和Goroutine提高Web的性能

    作為一門現代化編程語言,Go語言提供了強大的異步編程能力,使得程序員可以以更高效的方式處理并發(fā)任務,在Go語言中,使用Goroutine在單個進程中實現多任務并行處理,以及如何使用協程池來進一步提高Web服務器的處理能力,
    2024-01-01
  • Golang使用Gin實現文件上傳的示例代碼

    Golang使用Gin實現文件上傳的示例代碼

    本文我們主要介紹了Golang如何使用Gin實現文件上傳,Go標準庫net/http對文件上傳已經提供了非常完善的支持,而Gin框架在其基礎上進一步封裝,因此使用Gin開發(fā)文件上傳功能時,只需要簡單幾行代碼便可以實現,需要的朋友可以參考下
    2024-02-02
  • Golang常見錯誤之值拷貝和for循環(huán)中的單一變量詳解

    Golang常見錯誤之值拷貝和for循環(huán)中的單一變量詳解

    這篇文章主要給大家介紹了關于Golang常見錯誤之值拷貝和for循環(huán)中單一變量的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2017-11-11
  • Air實現Go程序實時熱重載使用過程解析示例

    Air實現Go程序實時熱重載使用過程解析示例

    這篇文章主要為大家介紹了Air實現Go程序實時熱重載使用過程解析示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪
    2022-04-04

最新評論