Golang的interface探究

golang被詬病最多的,沒有泛型應該算一個。做爲強類型語言來講,沒有泛型不少時候在業務開發上會有些不適應,可是它有個interface
類型,被不少人拿來當泛型玩,若是你瞭解它的原理也是沒問題的。
可是你真的瞭解嗎?golang

Interface

golang 中的interface,能夠將任意類型的變量賦予它。常見的咱們區分兩種,一種就是struct類型的,由於struct
可能會有func;另一種,就是非結構體的普通類型(下面提到的普通類型,都是指代除struct外的類型)安全

eface

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
  6     var x int
  7     var y interface{}
  8     x = 1
  9     y = x
 10     fmt.Println(y)
 11 }

當咱們把int類型的變量賦值給interface類型時,會發生什麼:併發

TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
    main.go:5    0x4a23a0    64488b0c25f8ffffff    mov rcx, qword ptr fs:[0xfffffff8]
    main.go:5    0x4a23a9    488d4424f8        lea rax, ptr [rsp-0x8]
    main.go:5    0x4a23ae    483b4110        cmp rax, qword ptr [rcx+0x10]
    main.go:5    0x4a23b2    0f86c7000000        jbe 0x4a247f
=>    main.go:5    0x4a23b8*    4881ec88000000        sub rsp, 0x88
    main.go:5    0x4a23bf    4889ac2480000000    mov qword ptr [rsp+0x80], rbp
    main.go:5    0x4a23c7    488dac2480000000    lea rbp, ptr [rsp+0x80]
    main.go:6    0x4a23cf    48c744243000000000    mov qword ptr [rsp+0x30], 0x0
    main.go:7    0x4a23d8    0f57c0            xorps xmm0, xmm0
    main.go:7    0x4a23db    0f11442448        movups xmmword ptr [rsp+0x48], xmm0
    main.go:8    0x4a23e0    48c744243001000000    mov qword ptr [rsp+0x30], 0x1
    main.go:9    0x4a23e9    48c7042401000000    mov qword ptr [rsp], 0x1
    main.go:9    0x4a23f1    e89a70f6ff        call $runtime.convT64

追到runtimeconvT64方法,一探究竟。函數

// type uint64InterfacePtr uint64
// var uint64Eface interface{} = uint64InterfacePtr(0)
// var uint64Type *_type = (*eface)(unsafe.Pointer(&uint64Eface))._type

func convT64(val uint64) (x unsafe.Pointer) {
    if val == 0 {
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(8, uint64Type, false)
        *(*uint64)(x) = val
    }
    return
}

這個方法返回了 val 的指針,其中uint64Type就是一個 0 值的uint64指針。有個疑問,這裏uint64Type定義時,eface 是什麼:ui

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

這個結構體,剛好知足了,對於普通類型轉換interface,或者說是將普通類型賦值給interface所必須的兩個字段,當前類型的type
(這裏貌似有點繞口)。真實的是,eface確實就是表示這類interface的結構體,在runtime中,還能看到其餘普通類型的轉換,
convTsliceconvTstringconvT64convT32等其餘幾個方法。atom

iface

若是是一個擁有funcstruct類型的變量,賦值給另外一個interface,這類的interface在底層是怎麼存的呢。以下所示:3d

1 package main                                                                                                                                                                                                                
  2 
  3 import "fmt"
  4 
  5 type Human interface{ Introduce() string }
  6 
  7 type Bob struct{ Human }
  8 
  9 func (b Bob) Introduce() string { return "Name: Bob" }
 10 
 11 func main() {
 12     var y Human
 13     x := Bob{}
 14     y = x
 15     fmt.Println(y)
 16 }
TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
        main.go:11      0x10b71a0       65488b0c2530000000              mov rcx, qword ptr gs:[0x30]
        main.go:11      0x10b71a9       488d4424d0                      lea rax, ptr [rsp-0x30]
        main.go:11      0x10b71ae       483b4110                        cmp rax, qword ptr [rcx+0x10]
        main.go:11      0x10b71b2       0f860f010000                    jbe 0x10b72c7
        ...省略部分指令
        main.go:14      0x10b7202       e84921f5ff                      call $runtime.convT2I

看彙編代碼,在 16 行時,調用了runtime.convT2I,這個方法返回的類型是iface指針

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
}

itab包括具體值的type和 interface 的type,還有其餘字段code

type itab struct {
    inter *interfacetype    // 接口定義的類型
    _type *_type            // 接口指向具體值的 type
    hash  uint32            // 類型的hash值
    _     [4]byte
    fun   [1]uintptr        // 判斷接口是否實現全部方法(下面會講到)
}

itab結構體的init方法中,是全部字段的初始化,重點看這個方法:協程

func (m *itab) init() string {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()

    // 在 interfacetype 的結構體中,mhdr 存着全部須要實現的方法的
    // 結構體切片 []imethod,都是按照方法名的字典序排列的,其中:
    // 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
    methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
    var fun0 unsafe.Pointer
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)
            // 比較已實現方法的 type 和 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 {
                        // 計算每一個 method 對應代碼塊的內存地址
                        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
                }
            }
        }
        // 若是沒有找到,將 func[0] 設置爲0,返回該實現的 method 的 name
        m.fun[0] = 0
        return iname
    }
    // 第一個方法的 ptr 和 type 的 hash
    m.fun[0] = uintptr(fun0)
    m.hash = typ.hash
    return ""
}

itabTable

還有一種將interface類型的實現,賦值給另一個interface

TEXT main.main(SB) /Users/such/gomodule/runtime/main.go
    ...省略部分指令
    main.go:18    0x10b71f5    488d842480000000        lea rax, ptr [rsp+0x80]
    main.go:18    0x10b71fd    4889442408            mov qword ptr [rsp+0x8], rax
    main.go:18    0x10b7202    e84921f5ff            call $runtime.convT2I
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
}

經過前面的分析,咱們又知道, iface 是由 tabdata 兩個字段組成。因此,實際上 convI2I 函數真正要作的事,
找到新 interfacetabdata,就大功告成了。在iface.go 文件頭部定義了itabTable全局哈希表存全部itab
其實就是空間換時間的思想。
itabTableitabTableType結構體(個人golang版本是1.12.7)

type itabTableType struct {
    size    uintptr             // 大小,2的冪
    count   uintptr             // 已有的 itab entry 個數
    entries [itabInitSize]*itab // 保存 itab entry
}
getitab

getitab是查找itab的方法

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
    t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    if m = t.find(inter, typ); m != nil {
        goto finish
    }

    // Not found.  Grab the lock and try again.
    lock(&itabLock)
    if m = itabTable.find(inter, typ); m != nil {
        unlock(&itabLock)
        goto finish
    }

    // Entry doesn't exist yet. Make a new entry & add it.
    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 := " 類型的斷言,會有panic
    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

會調用find方法,根據interfacetype_type的 hash 值,在itabTable中查找,找到的話直接返回;
不然,生成新的itab,加入 itabTable 中。有個問題,就是爲何第一次不加鎖找,而第二次加鎖?
我我的的理解是:`首先:應該仍是想避免鎖的開銷(以前在滴滴有幸聽過曹大分享【內存重排】,對經常使用package在concurrently時,鎖引發的問題作了一些分析。),
而第二次加鎖,我以爲更多的是在未找到 itab 後,會新生成一個 itab 寫入全局哈希表中,若是有其餘協程在查詢時,也未找到,能夠併發安全寫入。`

itabAdd
func itabAdd(m *itab) {
    if getg().m.mallocing != 0 {
        throw("malloc deadlock")
    }

    t := itabTable
    if t.count >= 3*(t.size/4) { // 75% load factor
        t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
        t2.size = t.size * 2
        
        iterate_itabs(t2.add)
        if t2.count != t.count {
            throw("mismatched count during itab table copy")
        }
        atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
        t = itabTable
    }
    t.add(m)
}

itabAdd 是添加itab加入itabTable的方法。既然是hash表,就必定會發生擴容。每次都
2的倍數的增加,建立新的 itabTable原子的替換。在 iterate_itabs(複製)時,並
未加鎖,這裏不是協程安全的,而是在添加前,在getitab方法中有鎖的操做,會等待複製完成。

相關文章
相關標籤/搜索