您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關go語言中接口的應用,以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
1. 接口的基本使用
golang中的interface本身也是一種類型,它代表的是一個方法的集合。任何類型只要實現了接口中聲明的所有方法,那么該類就實現了該接口。與其他語言不同,golang并不需要顯式聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。
聲明
type 接口名 interface{ 方法1 方法2 ... 方法n } type 接口名 interface { 已聲明接口名1 ... 已聲明接口名n } type iface interface{ tab *itab data unsafe.Pointer }
接口自身也是一種結構類型,只是編譯器對其做了很多限制:
● 不能有字段
● 不能定義自己的方法
● 只能聲明方法,不能實現
● 可嵌入其他接口類型
package main import ( "fmt" ) // 定義一個接口 type People interface { ReturnName() string } // 定義一個結構體 type Student struct { Name string } // 定義結構體的一個方法。 // 突然發現這個方法同接口People的所有方法(就一個),此時可直接認為結構體Student實現了接口People func (s Student) ReturnName() string { return s.Name } func main() { cbs := Student{Name:"小明"} var a People // 因為Students實現了接口所以直接賦值沒問題 // 如果沒實現會報錯:cannot use cbs (type Student) as type People in assignment:Student does not implement People (missing ReturnName method) a = cbs name := a.ReturnName() fmt.Println(name) // 輸出"小明" }
如果一個接口不包含任何方法,那么它就是一個空接口(empty interface),所有類型都符合empty interface的定義,因此任何類型都能轉換成empty interface。
接口的值簡單來說,是由兩部分組成的,就是類型和數據,判斷兩個接口是相等,就是看他們的這兩部分是否相等;另外類型和數據都為nil才代表接口是nil。
var a interface{} var b interface{} = (*int)(nil) fmt.Println(a == nil, b == nil) //true false
2. 接口嵌套
像匿名字段那樣嵌入其他接口。目標類型方法集中必須擁有包含嵌入接口方法在內的全部方法才算實現了該接口。嵌入其他接口類型相當于將其聲明的方法集中導入。這就要求不能有同名方法,不能嵌入自身或循環嵌入。
type stringer interfaceP{ string() string } type tester interface { stringer test() } type data struct{} func (*data) test() {} func (data) string () string { return "" } func main() { var d data var t tester = &d t.test() println(t.string()) }
超集接口變量可隱式轉換為子集,反過來不行。
3. 接口的實現
golang的接口檢測既有靜態部分,也有動態部分。
● 靜態部分
對于具體類型(concrete type,包括自定義類型) -> interface,編譯器生成對應的itab放到ELF的.rodata段,后續要獲取itab時,直接把指針指向存在.rodata的相關偏移地址即可。具體實現可以看golang的提交日志CL 20901、CL 20902。
對于interface->具體類型(concrete type,包括自定義類型),編譯器提取相關字段進行比較,并生成值
● 動態部分
在runtime中會有一個全局的hash表,記錄了相應type->interface類型轉換的itab,進行轉換時候,先到hash表中查,如果有就返回成功;如果沒有,就檢查這兩種類型能否轉換,能就插入到hash表中返回成功,不能就返回失敗。注意這里的hash表不是go中的map,而是一個最原始的使用數組的hash表,使用開放地址法來解決沖突。主要是interface <-> interface(接口賦值給接口、接口轉換成另一接口)使用到動態生產itab。
interface的結構如下:
接口類型的結構interfacetype
type interfacetype struct { typ _type pkgpath name //記錄定義接口的包名 mhdr []imethod //一個imethod切片,記錄接口中定義的那些函數。 } // imethod表示接口類型上的方法 type imethod struct { name nameOff // name of method typ typeOff // .(*FuncType) underneath }
nameOff 和 typeOff 類型是 int32 ,這兩個值是鏈接器負責嵌入的,相對于可執行文件的元信息的偏移量。元信息會在運行期,加載到 runtime.moduledata 結構體中。
4. 接口值的結構iface和eface
為了性能,golang專門分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口。
type iface struct { tab *itab data unsafe.Pointer } type eface struct { _type *_type data unsafe.Pointer } type itab struct { inter *interfacetype //inter接口類型 _type *_type //_type數據類型 hash uint32 //_type.hash的副本。用于類型開關。 hash哈希的方法 _ [4]byte fun [1]uintptr // 大小可變。 fun [0] == 0表示_type未實現inter。 fun函數地址占位符 }
iface結構體中的data是用來存儲實際數據的,runtime會申請一塊新的內存,把數據考到那,然后data指向這塊新的內存。
itab中的hash方法拷貝自_type.hash;fun是一個大小為1的uintptr數組,當fun[0]為0時,說明_type并沒有實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其他方法一次往下存放,這里就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了。
4.1 全局的itab table
iface.go:
const itabInitSize = 512 // 注意:如果更改這些字段,請在itabAdd的mallocgc調用中更改公式。 type itabTableType struct { size uintptr // 條目數組的長度。始終為2的冪。 count uintptr // 當前已填寫的條目數。 entries [itabInitSize]*itab // really [size] large }
可以看出這個全局的itabTable是用數組在存儲的,size記錄數組的大小,總是2的次冪。count記錄數組中已使用了多少。entries是一個*itab數組,初始大小是512。
5. 接口類型轉換
把一個具體的值,賦值給接口,會調用conv系列函數,例如空接口調用convT2E系列、非空接口調用convT2I系列,為了性能考慮,很多特例的convT2I64、convT2Estring諸如此類,避免了typedmemmove的調用。
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: 我們分配一個清零的對象只是為了用實際數據覆蓋它。 //確定如何避免歸零。同樣在下面的convT2Eslice,convT2I,convT2Islice中。 typedmemmove(t, x, elem) e._type = t e.data = x return } 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 } func convT2I16(tab *itab, val uint16) (i iface) { t := tab._type var x unsafe.Pointer if val == 0 { x = unsafe.Pointer(&zeroVal[0]) } else { x = mallocgc(2, t, false) *(*uint16)(x) = val } i.tab = tab i.data = x return } func convI2I(inter *interfacetype, i iface) (r iface) { tab := i.tab if tab == nil { return } if tab.inter == inter { r.tab = tab r.data = i.data return } r.tab = getitab(inter, tab._type, false) r.data = i.data return }
可以看出:
● 具體類型轉空接口,_type字段直接復制源的type;mallocgc一個新內存,把值復制過去,data再指向這塊內存。
● 具體類型轉非空接口,入參tab是編譯器生成的填進去的,接口指向同一個入參tab指向的itab;mallocgc一個新內存,把值復制過去,data再指向這塊內存。
● 對于接口轉接口,itab是調用getitab函數去獲取的,而不是編譯器傳入的。
對于那些特定類型的值,如果是零值,那么不會mallocgc一塊新內存,data會指向zeroVal[0]。
5.1 接口轉接口
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) { tab := i.tab if tab == nil { return } if tab.inter != inter { tab = getitab(inter, tab._type, true) if tab == nil { return } } r.tab = tab r.data = i.data b = true return } func assertE2I(inter *interfacetype, e eface) (r iface) { t := e._type if t == nil { // 顯式轉換需要非nil接口值。 panic(&TypeAssertionError{nil, nil, &inter.typ, ""}) } r.tab = getitab(inter, t, false) r.data = e.data return } func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) { t := e._type if t == nil { return } tab := getitab(inter, t, true) if tab == nil { return } r.tab = tab r.data = e.data b = true return }
我們看到有兩種用法:
● 返回值是一個時,不能轉換就panic。
● 返回值是兩個時,第二個返回值標記能否轉換成功
此外,data復制的是指針,不會完整拷貝值。每次都malloc一塊內存,那么性能會很差,因此,對于一些類型,golang的編譯器做了優化。
5.2 接口轉具體類型
接口判斷是否轉換成具體類型,是編譯器生成好的代碼去做的。我們看個empty interface轉換成具體類型的例子:
var EFace interface{} var j int func F4(i int) int{ EFace = I j = EFace.(int) return j } func main() { F4(10) }
反匯編:
go build -gcflags '-N -l' -o tmp build.go go tool objdump -s "main.F4" tmp
可以看匯編代碼:
MOVQ main.EFace(SB), CX //CX = EFace.typ2 LEAQ type.*+60128(SB), DX //DX = &type.int3 CMPQ DX, CX.
可以看到empty interface轉具體類型,是編譯器生成好對比代碼,比較具體類型和空接口是不是同一個type,而不是調用某個函數在運行時動態對比。
5.3 非空接口類型轉換
var tf Tester var t testStruct func F4() int{ t := tf.(testStruct) return t.i } func main() { F4() } //反匯編 MOVQ main.tf(SB), CX // CX = tf.tab(.inter.typ) LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對應的&itab(.inter.typ) CMPQ DX, CX //
可以看到,非空接口轉具體類型,也是編譯器生成的代碼,比較是不是同一個itab,而不是調用某個函數在運行時動態對比。
6. 獲取itab的流程
golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。看下代碼:
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { if len(inter.mhdr) == 0 { throw("internal error - misuse of itab") } // 簡單的情況 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()}) } var m *itab //首先,查看現有表以查看是否可以找到所需的itab。 //這是迄今為止最常見的情況,因此請不要使用鎖。 //使用atomic確保我們看到該線程完成的所有先前寫入更新itabTable字段(在itabAdd中使用atomic.Storep)。 t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable))) if m = t.find(inter, typ); m != nil { goto finish } // 未找到。抓住鎖,然后重試。 lock(&itabLock) if m = itabTable.find(inter, typ); m != nil { unlock(&itabLock) goto finish } // 條目尚不存在。進行新輸入并添加。 m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) m.inter = inter m._type = typ m.init() itabAdd(m) unlock(&itabLock) finish: if m.fun[0] != 0 { return m } if canfail { return nil } //僅當轉換時才會發生,使用ok形式已經完成一次,我們得到了一個緩存的否定結果。 //緩存的結果不會記錄,缺少接口函數,因此初始化再次獲取itab,以獲取缺少的函數名稱。 panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()}) }
流程如下:
● 先用t保存全局itabTable的地址,然后使用t.find去查找,這樣是為了防止查找過程中,itabTable被替換導致查找錯誤。
● 如果沒找到,那么就會上鎖,然后使用itabTable.find去查找,這樣是因為在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然后直接在itaTable中查找。
● 再沒找到,說明確實沒有,那么就根據接口類型、數據類型,去生成一個新的itab,然后插入到itabTable中,這里可能會導致hash表擴容,如果數據類型并沒有實現接口,那么根據調用方式,該報錯報錯,該panic panic。
這里我們可以看到申請新的itab空間時,內存空間的大小是unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因為字段fun是一個大小為1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。
6.1 在itabTable中查找itab find
func itabHashFunc(inter *interfacetype, typ *_type) uintptr { // 編譯器為我們提供了一些很好的哈希碼。 return uintptr(inter.typ.hash ^ typ.hash) } // find在t中找到給定的接口/類型對。 // 如果不存在給定的接口/類型對,則返回nil。 func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab { // 使用二次探測實現。 //探測順序為h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k。 //我們保證使用此探測序列擊中所有表條目。 mask := t.size - 1 h := itabHashFunc(inter, typ) & mask for i := uintptr(1); ; i++ { p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize)) // 在這里使用atomic read,所以如果我們看到m!= nil,我們也會看到m字段的初始化。 // m := *p m := (*itab)(atomic.Loadp(unsafe.Pointer(p))) if m == nil { return nil } if m.inter == inter && m._type == typ { return m } h += I h &= mask } }
從注釋可以看到,golang使用的開放地址探測法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據接口類型和數據類型的hash字段算出來的。以前的版本是額外使用一個link字段去連到下一個slot,那樣會有額外的存儲,性能也會差寫,在1.11中我們看到有了改進。
6.2 檢查并生成itab init
// init用所有代碼指針填充m.fun數組m.inter / m._type對。 如果該類型未實現該接口,將m.fun [0]設置為0,并返回缺少的接口函數的名稱。 //可以在同一m上多次調用此函數,即使同時調用也可以。 func (m *itab) init() string { inter := m.inter typ := m._type x := typ.uncommon() // inter和typ都有按名稱排序的方法, //并且接口名稱是唯一的, //因此可以在鎖定步驟中對兩者進行迭代; //循環是O(ni + nt)而不是O(ni * nt)。 ni := len(inter.mhdr) nt := int(x.mcount) xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] j := 0 imethods: for k := 0; k < ni; k++ { i := &inter.mhdr[k] 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] 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() } if tname.isExported() || pkgPath == ipkg { if m != nil { ifn := typ.textOff(t.ifn) *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn } continue imethods } } } // didn't find method m.fun[0] = 0 return iname } m.hash = typ.hash return "" }
這個方法會檢查interface和type的方法是否匹配,即type有沒有實現interface。假如interface有n中方法,type有m中方法,那么匹配的時間復雜度是O(n x m),由于interface、type的方法都按字典序排,所以O(n+m)的時間復雜度可以匹配完。在檢測的過程中,匹配上了,依次往fun字段寫入type中對應方法的地址。如果有一個方法沒有匹配上,那么就設置fun[0]為0,在外層調用會檢查fun[0]==0,即type并沒有實現interface。
這里我們還可以看到golang中continue的特殊用法,要直接continue到外層的循環中,那么就在那一層的循環上加個標簽,然后continue 標簽。
6.3 把itab插入到itabTable中 itabAdd
// itabAdd將給定的itab添加到itab哈希表中。 //必須保持itabLock。 func itabAdd(m *itab) { // 設置了mallocing時,錯誤可能導致調用此方法,通常是因為這是在恐慌時調用的。 //可靠地崩潰,而不是僅在需要增長時崩潰哈希表。 if getg().m.mallocing != 0 { throw("malloc deadlock") } t := itabTable if t.count >= 3*(t.size/4) { // 75% 負載系數 // 增長哈希表。 // t2 = new(itabTableType)+一些其他條目我們撒謊并告訴malloc我們想要無指針的內存,因為所有指向的值都不在堆中。 t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true)) t2.size = t.size * 2 // 復制條目。 //注意:在復制時,其他線程可能會尋找itab和找不到它。沒關系,他們將嘗試獲取Itab鎖,因此請等到復制完成。 if t2.count != t.count { throw("mismatched count during itab table copy") } // 發布新的哈希表。使用原子寫入:請參閱getitab中的注釋。 atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2)) // 采用新表作為我們自己的表。 t = itabTable // 注意:舊表可以在此處進行GC處理。 } t.add(m) } // add將給定的itab添加到itab表t中。 //必須保持itabLock。 func (t *itabTableType) add(m *itab) { //請參閱注釋中的有關探查序列的注釋。 //將新的itab插入探針序列的第一個空位。 mask := t.size - 1 h := itabHashFunc(m.inter, m._type) & mask for i := uintptr(1); ; i++ { p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize)) m2 := *p if m2 == m { //給定的itab可以在多個模塊中使用并且由于全局符號解析的工作方式, //指向itab的代碼可能已經插入了全局“哈希”。 return } if m2 == nil { // 在這里使用原子寫,所以如果讀者看到m,它也會看到正確初始化的m字段。 // NoWB正常,因為m不在堆內存中。 // *p = m atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m)) t.count++ return } h += I h &= mask } }
可以看到,當hash表使用達到75%或以上時,就會進行擴容,容量是原來的2倍,申請完空間,就會把老表中的數據插入到新的hash表中。然后使itabTable指向新的表,最后把新的itab插入到新表中。
上述就是小編為大家分享的go語言中接口的應用了,如果您也有類似的疑惑,不妨參照上述方法進行嘗試。如果想了解更多相關內容,請關注億速云行業資訊。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。