go中的數據結構-接口interface

1. 接口的基本使用

  golang中的interface本身也是一種類型,它代表的是一個方法的集合。任何類型只要實現了接口中聲明的所有方法,那麼該類就實現了該接口。與其他語言不同,golang並不需要顯式聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。

聲明

 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

2. 接口嵌套

  像匿名字段那樣嵌入其他接口。目標類型方法集中必須擁有包含嵌入接口方法在內的全部方法才算實現了該接口。嵌入其他接口類型相當於將其聲明的方法集中導入。這就要求不能有同名方法,不能嵌入自身或循環嵌入。

 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 }

  超集接口變量可隱式轉換為子集,反過來不行。

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

 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 結構體中。

4. 接口值的結構iface和eface

 為了性能,golang專門分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口。

 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字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了

4.1 全局的itab table

iface.go:

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。

5. 接口類型轉換

  把一個具體的值,賦值給接口,會調用conv系列函數,例如空接口調用convT2E系列、非空接口調用convT2I系列,為了性能考慮,很多特例的convT2I64、convT2Estring諸如此類,避免了typedmemmove的調用。

 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 }

  可以看出:

  • 具體類型轉空接口,_type字段直接複製源的type;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 具體類型轉非空接口,入參tab是編譯器生成的填進去的,接口指向同一個入參tab指向的itab;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 對於接口轉接口,itab是調用getitab函數去獲取的,而不是編譯器傳入的。

對於那些特定類型的值,如果是零值,那麼不會mallocgc一塊新內存,data會指向zeroVal[0]

5.1 接口轉接口

 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 }

  我們看到有兩種用法:

  • 返回值是一個時,不能轉換就panic。
  • 返回值是兩個時,第二個返回值標記能否轉換成功

  此外,data複製的是指針,不會完整拷貝值。每次都malloc一塊內存,那麼性能會很差,因此,對於一些類型,golang的編譯器做了優化。

5.2 接口轉具體類型

  接口判斷是否轉換成具體類型,是編譯器生成好的代碼去做的。我們看個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 }

反彙編:

  go build -gcflags ‘-N -l’ -o tmp build.go

  go tool objdump -s “main.F4” tmp

  可以看彙編代碼:

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,而不是調用某個函數在運行時動態對比。

5.3 非空接口類型轉換

 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,而不是調用某個函數在運行時動態對比。

6. 獲取itab的流程

  golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。看下代碼:

 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 }

  流程如下:

  • 先用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

 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中我們看到有了改進。

6.2 檢查並生成itab init

 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 標籤

6.3 把itab插入到itabTable中 itabAdd

 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插入到新表中。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步”網站設計“幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※試算大陸海運運費!