golang中的interface自己也是一種類型,它表明的是一個方法的集合。任何類型只要實現了接口中聲明的全部方法,那麼該類就實現了該接口。與其餘語言不一樣,golang並不須要顯式聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。golang
1 type 接口名 interface{ 2 方法1 3 方法2 4 ... 5 方法n 6 } 7 type 接口名 interface { 8 已聲明接口名1 9 ... 10 已聲明接口名n 11 } 12 type iface interface{ 13 tab *itab 14 data unsafe.Pointer 15 }
接口自身也是一種結構類型,只是編譯器對其作了不少限制:數組
1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 // 定義一個接口 8 type People interface { 9 ReturnName() string 10 } 11 12 // 定義一個結構體 13 type Student struct { 14 Name string 15 } 16 17 // 定義結構體的一個方法。 18 // 忽然發現這個方法同接口People的全部方法(就一個),此時可直接認爲結構體Student實現了接口People 19 func (s Student) ReturnName() string { 20 return s.Name 21 } 22 23 func main() { 24 cbs := Student{Name:"小明"} 25 26 var a People 27 // 由於Students實現了接口因此直接賦值沒問題 28 // 若是沒實現會報錯:cannot use cbs (type Student) as type People in assignment:Student does not implement People (missing ReturnName method) 29 a = cbs 30 name := a.ReturnName() 31 fmt.Println(name) // 輸出"小明" 32 }
若是一個接口不包含任何方法,那麼它就是一個空接口(empty interface),全部類型都符合empty interface的定義,所以任何類型都能轉換成empty interface。緩存
接口的值簡單來講,是由兩部分組成的,就是類型和數據,判斷兩個接口是相等,就是看他們的這兩部分是否相等;另外類型和數據都爲nil才表明接口是nil。函數
1 var a interface{} 2 var b interface{} = (*int)(nil) 3 fmt.Println(a == nil, b == nil) //true false
像匿名字段那樣嵌入其餘接口。目標類型方法集中必須擁有包含嵌入接口方法在內的所有方法纔算實現了該接口。嵌入其餘接口類型至關於將其聲明的方法集中導入。這就要求不能有同名方法,不能嵌入自身或循環嵌入。性能
1 type stringer interfaceP{ 2 string() string 3 } 4 5 type tester interface { 6 stringer 7 test() 8 } 9 10 type data struct{} 11 12 func (*data) test() {} 13 14 func (data) string () string { 15 return "" 16 } 17 18 func main() { 19 var d data 20 var t tester = &d 21 t.test() 22 println(t.string()) 23 }
超集接口變量可隱式轉換爲子集,反過來不行。優化
golang的接口檢測既有靜態部分,也有動態部分。ui
靜態部分
對於具體類型(concrete type,包括自定義類型) -> interface,編譯器生成對應的itab放到ELF的.rodata段,後續要獲取itab時,直接把指針指向存在.rodata的相關偏移地址便可。具體實現能夠看golang的提交日誌CL 2090一、CL 20902。
對於interface->具體類型(concrete type,包括自定義類型),編譯器提取相關字段進行比較,並生成值atom
動態部分
在runtime中會有一個全局的hash表,記錄了相應type->interface類型轉換的itab,進行轉換時候,先到hash表中查,若是有就返回成功;若是沒有,就檢查這兩種類型可否轉換,能就插入到hash表中返回成功,不能就返回失敗。注意這裏的hash表不是go中的map,而是一個最原始的使用數組的hash表,使用開放地址法來解決衝突。主要是interface <-> interface(接口賦值給接口、接口轉換成另外一接口)使用到動態生產itab。spa
interface的結構以下:線程
1 type interfacetype struct { 2 typ _type 3 pkgpath name //記錄定義接口的包名 4 mhdr []imethod //一個imethod切片,記錄接口中定義的那些函數。 5 } 6 7 // imethod表示接口類型上的方法 8 type imethod struct { 9 name nameOff // name of method 10 typ typeOff // .(*FuncType) underneath 11 }
nameOff 和 typeOff 類型是 int32 ,這兩個值是連接器負責嵌入的,相對於可執行文件的元信息的偏移量。元信息會在運行期,加載到 runtime.moduledata 結構體中。
1 type iface struct { 2 tab *itab 3 data unsafe.Pointer 4 } 5 6 type eface struct { 7 _type *_type 8 data unsafe.Pointer 9 } 10 11 type itab struct { 12 inter *interfacetype //inter接口類型 13 _type *_type //_type數據類型 14 hash uint32 //_type.hash的副本。用於類型開關。 hash哈希的方法 15 _ [4]byte 16 fun [1]uintptr // 大小可變。 fun [0] == 0表示_type未實現inter。 fun函數地址佔位符 17 }
iface結構體中的data是用來存儲實際數據的,runtime會申請一塊新的內存,把數據考到那,而後data指向這塊新的內存。
itab中的hash方法拷貝自_type.hash;fun是一個大小爲1的uintptr數組,當fun[0]爲0時,說明_type並無實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其餘方法一次往下存放,這裏就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了。
1 const itabInitSize = 512 2 3 // 注意:若是更改這些字段,請在itabAdd的mallocgc調用中更改公式。 4 type itabTableType struct { 5 size uintptr // 條目數組的長度。始終爲2的冪。 6 count uintptr // 當前已填寫的條目數。 7 entries [itabInitSize]*itab // really [size] large 8 }
能夠看出這個全局的itabTable是用數組在存儲的,size記錄數組的大小,老是2的次冪。count記錄數組中已使用了多少。entries是一個*itab數組,初始大小是512。
1 func convT2E(t *_type, elem unsafe.Pointer) (e eface) { 2 if raceenabled { 3 raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E)) 4 } 5 if msanenabled { 6 msanread(elem, t.size) 7 } 8 x := mallocgc(t.size, t, true) 9 // TODO: 咱們分配一個清零的對象只是爲了用實際數據覆蓋它。 10 //肯定如何避免歸零。一樣在下面的convT2Eslice,convT2I,convT2Islice中。 11 typedmemmove(t, x, elem) 12 e._type = t 13 e.data = x 14 return 15 } 16 17 func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { 18 t := tab._type 19 if raceenabled { 20 raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I)) 21 } 22 if msanenabled { 23 msanread(elem, t.size) 24 } 25 x := mallocgc(t.size, t, true) 26 typedmemmove(t, x, elem) 27 i.tab = tab 28 i.data = x 29 return 30 } 31 32 func convT2I16(tab *itab, val uint16) (i iface) { 33 t := tab._type 34 var x unsafe.Pointer 35 if val == 0 { 36 x = unsafe.Pointer(&zeroVal[0]) 37 } else { 38 x = mallocgc(2, t, false) 39 *(*uint16)(x) = val 40 } 41 i.tab = tab 42 i.data = x 43 return 44 } 45 46 func convI2I(inter *interfacetype, i iface) (r iface) { 47 tab := i.tab 48 if tab == nil { 49 return 50 } 51 if tab.inter == inter { 52 r.tab = tab 53 r.data = i.data 54 return 55 } 56 r.tab = getitab(inter, tab._type, false) 57 r.data = i.data 58 return 59 }
能夠看出:
對於那些特定類型的值,若是是零值,那麼不會mallocgc一塊新內存,data會指向zeroVal[0]。
1 func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) { 2 tab := i.tab 3 if tab == nil { 4 return 5 } 6 if tab.inter != inter { 7 tab = getitab(inter, tab._type, true) 8 if tab == nil { 9 return 10 } 11 } 12 r.tab = tab 13 r.data = i.data 14 b = true 15 return 16 } 17 18 func assertE2I(inter *interfacetype, e eface) (r iface) { 19 t := e._type 20 if t == nil { 21 // 顯式轉換須要非nil接口值。 22 panic(&TypeAssertionError{nil, nil, &inter.typ, ""}) 23 } 24 r.tab = getitab(inter, t, false) 25 r.data = e.data 26 return 27 } 28 29 func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) { 30 t := e._type 31 if t == nil { 32 return 33 } 34 tab := getitab(inter, t, true) 35 if tab == nil { 36 return 37 } 38 r.tab = tab 39 r.data = e.data 40 b = true 41 return 42 }
咱們看到有兩種用法:
此外,data複製的是指針,不會完整拷貝值。每次都malloc一塊內存,那麼性能會不好,所以,對於一些類型,golang的編譯器作了優化。
接口判斷是否轉換成具體類型,是編譯器生成好的代碼去作的。咱們看個empty interface轉換成具體類型的例子:
1 var EFace interface{} 2 var j int 3 4 func F4(i int) int{ 5 EFace = I 6 j = EFace.(int) 7 return j 8 } 9 10 func main() { 11 F4(10) 12 }
1 MOVQ main.EFace(SB), CX //CX = EFace.typ 2 LEAQ type.*+60128(SB), DX //DX = &type.int 3 CMPQ DX, CX. //if DX == AX
能夠看到empty interface轉具體類型,是編譯器生成好對比代碼,比較具體類型和空接口是否是同一個type,而不是調用某個函數在運行時動態對比。
1 var tf Tester 2 var t testStruct 3 4 func F4() int{ 5 t := tf.(testStruct) 6 return t.i 7 } 8 9 func main() { 10 F4() 11 } 12 //反彙編 13 MOVQ main.tf(SB), CX // CX = tf.tab(.inter.typ) 14 LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對應的&itab(.inter.typ) 15 CMPQ DX, CX //
能夠看到,非空接口轉具體類型,也是編譯器生成的代碼,比較是否是同一個itab,而不是調用某個函數在運行時動態對比。
1 func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { 2 if len(inter.mhdr) == 0 { 3 throw("internal error - misuse of itab") 4 } 5 6 // 簡單的狀況 7 if typ.tflag&tflagUncommon == 0 { 8 if canfail { 9 return nil 10 } 11 name := inter.typ.nameOff(inter.mhdr[0].name) 12 panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()}) 13 } 14 15 var m *itab 16 17 //首先,查看現有表以查看是否能夠找到所需的itab。 18 //這是迄今爲止最多見的狀況,所以請不要使用鎖。 19 //使用atomic確保咱們看到該線程完成的全部先前寫入更新itabTable字段(在itabAdd中使用atomic.Storep)。 20 t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable))) 21 if m = t.find(inter, typ); m != nil { 22 goto finish 23 } 24 25 // 未找到。抓住鎖,而後重試。 26 lock(&itabLock) 27 if m = itabTable.find(inter, typ); m != nil { 28 unlock(&itabLock) 29 goto finish 30 } 31 32 // 條目尚不存在。進行新輸入並添加。 33 m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) 34 m.inter = inter 35 m._type = typ 36 m.init() 37 itabAdd(m) 38 unlock(&itabLock) 39 finish: 40 if m.fun[0] != 0 { 41 return m 42 } 43 if canfail { 44 return nil 45 } 46 //僅當轉換時纔會發生,使用ok形式已經完成一次,咱們獲得了一個緩存的否認結果。 47 //緩存的結果不會記錄,缺乏接口函數,所以初始化再次獲取itab,以獲取缺乏的函數名稱。 48 panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()}) 49 }
流程以下:
1 func itabHashFunc(inter *interfacetype, typ *_type) uintptr { 2 // 編譯器爲咱們提供了一些很好的哈希碼。 3 return uintptr(inter.typ.hash ^ typ.hash) 4 } 5 6 // find在t中找到給定的接口/類型對。 7 // 若是不存在給定的接口/類型對,則返回nil。 8 func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab { 9 // 使用二次探測實現。 10 //探測順序爲h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k。 11 //咱們保證使用此探測序列擊中全部表條目。 12 mask := t.size - 1 13 h := itabHashFunc(inter, typ) & mask 14 for i := uintptr(1); ; i++ { 15 p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize)) 16 // 在這裏使用atomic read,因此若是咱們看到m!= nil,咱們也會看到m字段的初始化。 17 // m := *p 18 m := (*itab)(atomic.Loadp(unsafe.Pointer(p))) 19 if m == nil { 20 return nil 21 } 22 if m.inter == inter && m._type == typ { 23 return m 24 } 25 h += I 26 h &= mask 27 } 28 }
從註釋能夠看到,golang使用的開放地址探測法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據接口類型和數據類型的hash字段算出來的。之前的版本是額外使用一個link字段去連到下一個slot,那樣會有額外的存儲,性能也會差寫,在1.11中咱們看到有了改進。
1 // init用全部代碼指針填充m.fun數組m.inter / m._type對。 若是該類型未實現該接口,將m.fun [0]設置爲0,並返回缺乏的接口函數的名稱。 2 //能夠在同一m上屢次調用此函數,即便同時調用也能夠。 3 func (m *itab) init() string { 4 inter := m.inter 5 typ := m._type 6 x := typ.uncommon() 7 8 // inter和typ都有按名稱排序的方法, 9 //而且接口名稱是惟一的, 10 //所以能夠在鎖定步驟中對二者進行迭代; 11 //循環是O(ni + nt)而不是O(ni * nt)。 12 ni := len(inter.mhdr) 13 nt := int(x.mcount) 14 xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] 15 j := 0 16 imethods: 17 for k := 0; k < ni; k++ { 18 i := &inter.mhdr[k] 19 itype := inter.typ.typeOff(i.ityp) 20 name := inter.typ.nameOff(i.name) 21 iname := name.name() 22 ipkg := name.pkgPath() 23 if ipkg == "" { 24 ipkg = inter.pkgpath.name() 25 } 26 for ; j < nt; j++ { 27 t := &xmhdr[j] 28 tname := typ.nameOff(t.name) 29 if typ.typeOff(t.mtyp) == itype && tname.name() == iname { 30 pkgPath := tname.pkgPath() 31 if pkgPath == "" { 32 pkgPath = typ.nameOff(x.pkgpath).name() 33 } 34 if tname.isExported() || pkgPath == ipkg { 35 if m != nil { 36 ifn := typ.textOff(t.ifn) 37 *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn 38 } 39 continue imethods 40 } 41 } 42 } 43 // didn't find method 44 m.fun[0] = 0 45 return iname 46 } 47 m.hash = typ.hash 48 return "" 49 }
這個方法會檢查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 標籤。
1 // itabAdd將給定的itab添加到itab哈希表中。 2 //必須保持itabLock。 3 func itabAdd(m *itab) { 4 // 設置了mallocing時,錯誤可能致使調用此方法,一般是由於這是在恐慌時調用的。 5 //可靠地崩潰,而不是僅在須要增加時崩潰哈希表。 6 if getg().m.mallocing != 0 { 7 throw("malloc deadlock") 8 } 9 10 t := itabTable 11 if t.count >= 3*(t.size/4) { // 75% 負載係數 12 // 增加哈希表。 13 // t2 = new(itabTableType)+一些其餘條目咱們撒謊並告訴malloc咱們想要無指針的內存,由於全部指向的值都不在堆中。 14 t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true)) 15 t2.size = t.size * 2 16 17 // 複製條目。 18 //注意:在複製時,其餘線程可能會尋找itab和找不到它。不要緊,他們將嘗試獲取Itab鎖,所以請等到複製完成。 19 if t2.count != t.count { 20 throw("mismatched count during itab table copy") 21 } 22 // 發佈新的哈希表。使用原子寫入:請參閱getitab中的註釋。 23 atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2)) 24 // 採用新表做爲咱們本身的表。 25 t = itabTable 26 // 注意:舊錶能夠在此處進行GC處理。 27 } 28 t.add(m) 29 } 30 // add將給定的itab添加到itab表t中。 31 //必須保持itabLock。 32 func (t *itabTableType) add(m *itab) { 33 //請參閱註釋中的有關探查序列的註釋。 34 //將新的itab插入探針序列的第一個空位。 35 mask := t.size - 1 36 h := itabHashFunc(m.inter, m._type) & mask 37 for i := uintptr(1); ; i++ { 38 p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize)) 39 m2 := *p 40 if m2 == m { 41 //給定的itab能夠在多個模塊中使用而且因爲全局符號解析的工做方式, 42 //指向itab的代碼可能已經插入了全局「哈希」。 43 return 44 } 45 if m2 == nil { 46 // 在這裏使用原子寫,因此若是讀者看到m,它也會看到正確初始化的m字段。 47 // NoWB正常,由於m不在堆內存中。 48 // *p = m 49 atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m)) 50 t.count++ 51 return 52 } 53 h += I 54 h &= mask 55 } 56 }
能夠看到,當hash表使用達到75%或以上時,就會進行擴容,容量是原來的2倍,申請完空間,就會把老表中的數據插入到新的hash表中。而後使itabTable指向新的表,最後把新的itab插入到新表中。