剖析golang interface實(shí)現(xiàn)

[TOC]


本文基于golang 1.11源碼進(jìn)行分析。先演示用法和注意事項(xiàng),再深入源碼分析原理。

在golang中,接口本身也是一種類(lèi)型,它代表的是一個(gè)方法的集合。任何類(lèi)型只要實(shí)現(xiàn)了接口中聲明的所有方法,那么該類(lèi)就實(shí)現(xiàn)了該接口。與其他語(yǔ)言不同,golang并不需要顯示聲明類(lèi)型實(shí)現(xiàn)了某個(gè)接口,而是由編譯器和runtime進(jìn)行檢查。不用顯示什么這點(diǎn)非常棒,這樣就無(wú)侵入非常方便。

1 使用

1.1 聲明

type 接口名 interface {
    方法1
    方法2
    ...
    方法n
}

type 接口名 interface {
    已聲明接口名1
    ...
    已聲明接口名n
}

如果一個(gè)接口不包含任何方法,那么就是一個(gè)空接口(empty interface),所有類(lèi)型都符合empty interface的定義,因此任何類(lèi)型都能轉(zhuǎn)換成empty interface,可以看到常常使用empty interface來(lái)實(shí)現(xiàn)多態(tài),例如之前我們分析過(guò)的map源碼。

對(duì)于接口的命名,一般我們都是以er結(jié)尾,例如Writer、Reader等等。


1.2 實(shí)現(xiàn)接口

我們看個(gè)例子:

package main

import (
    "fmt"
)

type Tester interface {
    Display()
    DisplayAppend(string)
    DisplayAppend2(string) string
}

type Tester2 interface {
    DisplayAppend(string)
}

type Test struct {
    s string
}

func (t *Test) Display() {
    fmt.Printf("Display:%p, %#v\n", t ,t)
}

func (t Test) DisplayAppend(s string) {
    t.s += s
    fmt.Printf("DisplayAppend:%p, %#v\n", &t, t)
}

func (t *Test) DisplayAppend2 (s string) string {
    t.s += s    
    fmt.Printf("DisplayAppend2:%p, %#v\n", t, t)
    return t.s
}

func TestInterface(t Tester) {
    t.Display()
    t.DisplayAppend(" TestInterface")
    t.DisplayAppend2(" TestInterface")
}

func TestInterface2(t Tester2) {
    t.DisplayAppend("TestInterface2")
}

func main() {
    var test Test
    test.s = "aaa"
    fmt.Printf("%p\n", &test)
    test.Display()
    test.DisplayAppend(" raw")

    TestInterface(&test)
    //TestInterface(test) //cannot use test (type Test) as type Tester in argument to TestInterface:Test does not implement Tester (Display method has pointer receiver)
    
    TestInterface2(&test)
    TestInterface2(test)
}

輸出
0xc42000e1e0
Display:0xc42000e1e0, &main.Test{s:"aaa"}
DisplayAppend:0xc42000e200, main.Test{s:"aaa raw"}
Display:0xc42000e1e0, &main.Test{s:"aaa"}
DisplayAppend:0xc42000e230, main.Test{s:"aaa TestInterface"}
DisplayAppend2:0xc42000e1e0, &main.Test{s:"aaa TestInterface"}
DisplayAppend:0xc42000e260, main.Test{s:"aaa TestInterfaceTestInterface2"}
DisplayAppend:0xc42000e290, main.Test{s:"aaa TestInterfaceTestInterface2"}

在這個(gè)例子中,我們定義了一個(gè)類(lèi)型Test,Test類(lèi)型有三個(gè)方法,兩個(gè)方法的接受者是*Test,一個(gè)方法的接受者是Test;定義兩個(gè)接口類(lèi)型,Tester和Tester2 ,Tester有三個(gè)Test中的同名方法,Tester2中只有接受者是Test的同名方法。然后有TestInterface函數(shù),入?yún)⑹荰ester,TestInterface2函數(shù)入?yún)⑹荰ester2。

從編譯和運(yùn)行結(jié)果可以看到,TestInterface函數(shù)入?yún)⒅荒芴頣est類(lèi)型,TestInterface2入?yún)⒓瓤梢允荰est也可以是*Test。TestInterface傳入Test類(lèi)型變量test時(shí),編譯報(bào)錯(cuò):

cannot use test (type Test1) as type Tester in argument to TestInterface:Test1 does not implement Tester (Display method has pointer receiver)

意思是說(shuō)test并沒(méi)有實(shí)現(xiàn)Tester的Display接口,因?yàn)間olang中,類(lèi)型T只有接受者是T的方法,語(yǔ)法中T能直接調(diào)*T的方法僅僅是語(yǔ)法糖;而類(lèi)型*T擁有接受者是T和*T的方法。


1.3 類(lèi)型判斷

傳入?yún)?shù)接口的時(shí)候,如果我們希望確切知道它具體類(lèi)型,那么就要用到類(lèi)型判斷了。有兩種類(lèi)型判斷方法:

變量.(類(lèi)型) //判斷是不是某個(gè)具體類(lèi)型

switch 變量.(type) //返回具體類(lèi)型,必須搭配swith語(yǔ)句

talk is cheap,看代碼:

package main

import (
    "fmt"
)

type Tester interface {
    Display()
    DisplayAppend(string)
    DisplayAppend2(string) string
}

type Tester2 interface {
    DisplayAppend(string)
}

type Test1 struct {
    s string
}

func (t *Test1) Display() {
    fmt.Printf("Display:%p, %#v\n", t ,t)
}

func (t Test1) DisplayAppend(s string) {
    t.s += s
    fmt.Printf("DisplayAppend:%p, %#v\n", &t, t)
}

func (t *Test1) DisplayAppend2 (s string) string {
    t.s += s    
    fmt.Printf("DisplayAppend2:%p, %#v\n", t, t)
    return t.s
}

func TestInterface(t Tester) {
    t.Display()
    t.DisplayAppend(" TestInterface")
    t.DisplayAppend2(" TestInterface")
}

func TestInterface2(t Tester2) {
    t.DisplayAppend("TestInterface2")
}

func Printf(t interface{})  {
    if v, ok := t.(int); ok {
        v = 2
        fmt.Printf("type[%T] %v %v\n", v, v, t)
    } 

    if v, ok := t.(int32);ok {
        fmt.Printf("type[%T] %v\n", v, v)
    } 

    if v, ok := t.(int64); ok {
        fmt.Printf("type[%T] %v\n", v, v)
    } 

    if v, ok := t.(Tester2); ok  {
        fmt.Printf("type[%T] %v\n", v, v)
    }

    if v, ok := t.(Tester); ok  {
        fmt.Printf("type[%T] %v\n", v, v)
    }

}

func Printf2(v interface{}) {
    fmt.Printf("%p %v\n", &v, v)
    switch v := v.(type) {
        case nil:
            fmt.Printf("type[%T] %v\n", v, v)
        case int: 
        fmt.Printf("%p %v\n", &v, v)
            fmt.Printf("type[%T] %v\n", v, v)
        case int64:
            fmt.Printf("type[%T] %v\n", v, v)
        case string:
            fmt.Printf("type[%T] %v\n", v, v)
        case Tester:
            fmt.Printf("tester type[%T] %v\n", v, v)
        case Tester2:
            fmt.Printf("tester2 type[%T] %v\n", v, v)
        default:
            fmt.Printf("unknow\n")
    }
}

func main() {
    var i int64 = 1
    Printf(i)

    var i2 int = 1
    Printf(i2)

    var test Test1
    Printf(test)
    Printf(&test)

    fmt.Printf("------------\n")
    Printf2(i2)
    Printf2(test)
    Printf2(&test)
}

輸出
type[int64] 1
type[int] 2 1
type[main.Test1] {}
type[main.Test1] &{}
type[
main.Test1] &{}
------------
0xc42000e220 1
0xc4200160b8 1
type[int] 1
0xc42000e240 {}
tester2 type[main.Test1] {}
0xc42000e250 &{}
tester type[*main.Test1] &{}

從這里我們可以看出兩個(gè)點(diǎn):

  1. 對(duì)于判斷接口,只要實(shí)現(xiàn)該接口就能匹配上
  2. 類(lèi)型判斷返回也是按值復(fù)制,修改返回的value,不影響原來(lái)的值
  3. 接口的匹配,是嚴(yán)格的匹配,并不是說(shuō)接口1能轉(zhuǎn)換成接口2他們就能匹配上

golang中,我們經(jīng)常用類(lèi)型判斷來(lái)判斷特定的錯(cuò)誤。


1.4 接口的值

接口的值簡(jiǎn)單來(lái)說(shuō),是由兩部分組成的,就是類(lèi)型和數(shù)據(jù),詳細(xì)的組成會(huì)在下面的實(shí)現(xiàn)章節(jié)中說(shuō)明。
那么判斷兩個(gè)接口是相等,就是看他們的這兩部分是否相等;另外類(lèi)型和數(shù)據(jù)都為nil才代表接口是nil,eg:

    var a interface{} 
    var b interface{} = (*int)(nil)
    fmt.Println(a == nil, b == nil) //true false

這點(diǎn)很重要,很多人在實(shí)現(xiàn)error接口的時(shí)候判斷錯(cuò)誤,下面我們看個(gè)例子:

type MyError struct{}
func (*MyError) Error() string {
    return "my error"
}

func TestError(x int) (int, error) {
    var err *MyError
    if x < 0 {
        err = new(MyError)
    }

    return x, err
}

func main() {
    var err error
    _, err = TestError(10)
    fmt.Println(err == nil) //false
}

在x大于0時(shí),TestError中的err是nil,返回的時(shí)候,轉(zhuǎn)換成error類(lèi)型,那么類(lèi)型就是(*MyError),值是nil,由于類(lèi)型不是nil,所以最終返回給調(diào)用方的總是失敗。


2 實(shí)現(xiàn)

如前面所說(shuō),golang中你不需要聲明一個(gè)類(lèi)型實(shí)現(xiàn)了那些接口,這帶來(lái)了方便,但是實(shí)現(xiàn)上會(huì)比那些需要聲明的語(yǔ)言更加復(fù)雜。golang的接口檢測(cè)既有靜態(tài)部分,也有動(dòng)態(tài)部分。

  • 靜態(tài)部分
    對(duì)于具體類(lèi)型(concrete type,包括自定義類(lèi)型) -> interface,編譯器生成對(duì)應(yīng)的itab放到ELF的.rodata段,后續(xù)要獲取itab時(shí),直接把指針指向存在.rodata的相關(guān)偏移地址即可。具體實(shí)現(xiàn)可以看golang的提交日志CL 20901、CL 20902。
    對(duì)于interface->具體類(lèi)型(concrete type,包括自定義類(lèi)型),編譯器提取相關(guān)字段進(jìn)行比較,并生成值

  • 動(dòng)態(tài)部分
    在runtime中會(huì)有一個(gè)全局的hash表,記錄了相應(yīng)type->interface類(lèi)型轉(zhuǎn)換的itab,進(jìn)行轉(zhuǎn)換時(shí)候,先到hash表中查,如果有就返回成功;如果沒(méi)有,就檢查這兩種類(lèi)型能否轉(zhuǎn)換,能就插入到hash表中返回成功,不能就返回失敗。注意這里的hash表不是go中的map,而是一個(gè)最原始的使用數(shù)組的hash表,使用開(kāi)放地址法來(lái)解決沖突。主要是interface <-> interface(接口賦值給接口、接口轉(zhuǎn)換成另一接口)使用到動(dòng)態(tài)生產(chǎn)itab


2.1 結(jié)構(gòu)

interface結(jié)構(gòu)圖

2.1.1 接口類(lèi)型的結(jié)構(gòu) interfacetype

type interfacetype struct {
    typ     _type   
    pkgpath name
    mhdr    []imethod
}

// imethod represents a method on an interface type
type imethod struct {
    name nameOff // name of method
    typ  typeOff // .(*FuncType) underneath
}

pkgpath記錄定義接口的包名
其中的mdhr字段,是一個(gè)imethod切片,記錄接口中定義的那些函數(shù)。
nameOff 和 typeOff 類(lèi)型是 int32 ,這兩個(gè)值是鏈接器負(fù)責(zé)嵌入的,相對(duì)于可執(zhí)行文件的元信息的偏移量。元信息會(huì)在運(yùn)行期,加載到 runtime.moduledata 結(jié)構(gòu)體中。

2.1.2 接口值的結(jié)構(gòu) iface eface

為了性能,golang專(zhuān)門(mén)分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口
結(jié)構(gòu)定義分別在兩個(gè)文件中,runtime2.go:

type iface struct { 
    tab  *itab
    data unsafe.Pointer
}

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

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

data字段是用來(lái)存儲(chǔ)實(shí)際數(shù)據(jù)的,runtime會(huì)申請(qǐng)一塊新的內(nèi)存,把數(shù)據(jù)考到那,然后data指向這塊新的內(nèi)存

eface、itab的_tpye字段,是data指向的值的實(shí)際類(lèi)型信息

iface中tab字段,是一個(gè)itab結(jié)構(gòu),包含了inter接口類(lèi)型、_type數(shù)據(jù)類(lèi)型、hash哈希的方法、fun函數(shù)地址占位符。這個(gè)hash方法拷貝自_type.hash;fun是一個(gè)大小為1的uintptr數(shù)組,當(dāng)fun[0]為0時(shí),說(shuō)明_type并沒(méi)有實(shí)現(xiàn)該接口,當(dāng)有實(shí)現(xiàn)接口時(shí),fun存放了第一個(gè)接口方法的地址,其他方法一次往下存放,這里就簡(jiǎn)單用空間換時(shí)間,其實(shí)方法都在_type字段中能找到,實(shí)際在這記錄下,每次調(diào)用的時(shí)候就不用動(dòng)態(tài)查找了

2.1.2 全局的itab table

iface.go:

const itabInitSize = 512

// Note: change the formula in the mallocgc call in itabAdd if you change these fields.
type itabTableType struct {
    size    uintptr             // length of entries array. Always a power of 2.
    count   uintptr             // current number of filled entries.
    entries [itabInitSize]*itab // really [size] large
}

可以看出這個(gè)全局的itabTable是用數(shù)組在存儲(chǔ)的
size記錄數(shù)組的大小,總是2的次冪。
count記錄數(shù)組中已使用了多少。
entries是一個(gè)*itab數(shù)組,初始大小是512.


2.2 轉(zhuǎn)換

把一個(gè)具體的值,賦值給接口,會(huì)調(diào)用conv系列函數(shù),例如空接口調(diào)用convT2E系列、非空接口調(diào)用convT2I系列,為了性能考慮,很多特例的convT2I64、convT2Estring諸如此類(lèi),避免了typedmemmove的調(diào)用。

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: We allocate a zeroed object only to overwrite it with actual data.
    // Figure out how to avoid zeroing. Also below in 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
}

可以看到:

  1. 具體類(lèi)型轉(zhuǎn)空接口,_type字段直接復(fù)制源的type;mallocgc一個(gè)新內(nèi)存,把值復(fù)制過(guò)去,data再指向這塊內(nèi)存。
  2. 具體類(lèi)型轉(zhuǎn)非空接口,入?yún)ab是編譯器生成的填進(jìn)去的,接口指向同一個(gè)入?yún)ab指向的itab;mallocgc一個(gè)新內(nèi)存,把值復(fù)制過(guò)去,data再指向這塊內(nèi)存。
  3. 對(duì)于接口轉(zhuǎn)接口,itab是調(diào)用getitab函數(shù)去獲取的,而不是編譯器傳入的。

對(duì)于那些特定類(lèi)型的值,如果是零值,那么不會(huì)mallocgc一塊新內(nèi)存,data會(huì)指向zeroVal[0]。

2.2.1 編譯器優(yōu)化

每次都malloc一塊內(nèi)存,那么性能會(huì)很差,因此,對(duì)于一些類(lèi)型,golang的編譯器做了優(yōu)化。 TODO


2.3 獲取itab的流程

golang interface的核心邏輯就在這,在get的時(shí)候,不僅僅會(huì)從itabTalbe中查找,還可能會(huì)創(chuàng)建插入,itabTable使用容量超過(guò)75%還會(huì)擴(kuò)容。下面我們看下代碼:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    if len(inter.mhdr) == 0 {
        throw("internal error - misuse of itab")
    }

    // easy case
    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

    // First, look in the existing table to see if we can find the itab we need.
    // This is by far the most common case, so do it without locks.
    // Use atomic to ensure we see any previous writes done by the thread
    // that updates the itabTable field (with atomic.Storep in itabAdd).
    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
    }
    // this can only happen if the conversion
    // was already done once using the , ok form
    // and we have a cached negative result.
    // The cached result doesn't record which
    // interface function was missing, so initialize
    // the itab again to get the missing function name.
    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}

流程很簡(jiǎn)單

  1. 先用t保存全局itabTable的地址,然后使用t.find去查找,這樣是為了防止查找過(guò)程中,itabTable被替換導(dǎo)致查找錯(cuò)誤。
  2. 如果沒(méi)找到,那么就會(huì)上鎖,然后使用itabTable.find去查找,這樣是因?yàn)樵诘谝徊讲檎业耐瑫r(shí),另外一個(gè)協(xié)程寫(xiě)入,可能導(dǎo)致實(shí)際存在卻查找不到,這時(shí)上鎖避免itabTable被替換,然后直接在itaTable中查找。
  3. 再?zèng)]找到,說(shuō)明確實(shí)沒(méi)有,那么就根據(jù)接口類(lèi)型、數(shù)據(jù)類(lèi)型,去生成一個(gè)新的itab,然后插入到itabTable中,這里可能會(huì)導(dǎo)致hash表擴(kuò)容,如果數(shù)據(jù)類(lèi)型并沒(méi)有實(shí)現(xiàn)接口,那么根據(jù)調(diào)用方式,該報(bào)錯(cuò)報(bào)錯(cuò),該panic panic。

這里我們可以看到申請(qǐng)新的itab空間時(shí),內(nèi)存空間的大小是unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結(jié)構(gòu),len(inter.mhdr)就是接口定義的方法數(shù)量,因?yàn)樽侄蝔un是一個(gè)大小為1的數(shù)組,所以len(inter.mhdr)-1,在fun字段下面其實(shí)隱藏了其他方法接口地址。

然后我們?cè)倏聪律厦嬗玫降囊恍┓椒ǖ募?xì)節(jié)

2.3.1 在itabTable中查找itab find

func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
    // compiler has provided some good hash codes for us.
    return uintptr(inter.typ.hash ^ typ.hash)
}

// find finds the given interface/type pair in t.
// Returns nil if the given interface/type pair isn't present.
func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
    // Implemented using quadratic probing.
    // Probe sequence is h(i) = h0 + i*(i+1)/2 mod 2^k.
    // We're guaranteed to hit all table entries using this probe sequence.
    mask := t.size - 1
    h := itabHashFunc(inter, typ) & mask
    for i := uintptr(1); ; i++ {
        p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
        // Use atomic read here so if we see m != nil, we also see
        // the initializations of the fields of 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使用的開(kāi)放地址探測(cè)法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據(jù)接口類(lèi)型和數(shù)據(jù)類(lèi)型的hash字段算出來(lái)的。以前的版本是額外使用一個(gè)link字段去連到下一個(gè)slot,那樣會(huì)有額外的存儲(chǔ),性能也會(huì)差寫(xiě),在1.11中我們看到做了改進(jìn),具體是哪個(gè)版本開(kāi)始變的我也不知道。

2.3.2 檢查并生成itab init

// init fills in the m.fun array with all the code pointers for
// the m.inter/m._type pair. If the type does not implement the interface,
// it sets m.fun[0] to 0 and returns the name of an interface function that is missing.
// It is ok to call this multiple times on the same m, even concurrently.
func (m *itab) init() string {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()

    // both inter and typ have method sorted by name,
    // and interface names are unique,
    // so can iterate over both in lock step;
    // the loop is O(ni+nt) not 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 ""
}

這個(gè)方法會(huì)檢查interface和type的方法是否匹配,即type有沒(méi)有實(shí)現(xiàn)interface。假如interface有n中方法,type有m中方法,那么匹配的時(shí)間復(fù)雜度是O(n x m),由于interface、type的方法都按字典序排,所以O(shè)(n+m)的時(shí)間復(fù)雜度可以匹配完。在檢測(cè)的過(guò)程中,匹配上了,依次往fun字段寫(xiě)入type中對(duì)應(yīng)方法的地址。如果有一個(gè)方法沒(méi)有匹配上,那么就設(shè)置fun[0]為0,在外層調(diào)用會(huì)檢查fun[0]==0,即type并沒(méi)有實(shí)現(xiàn)interface。

這里我們還可以看到golang中continue的特殊用法,要直接continue到外層的循環(huán)中,那么就在那一層的循環(huán)上加個(gè)標(biāo)簽,然后continue 標(biāo)簽。

2.3.3 把itab插入到itabTable中 itabAdd

// itabAdd adds the given itab to the itab hash table.
// itabLock must be held.
func itabAdd(m *itab) {
    // Bugs can lead to calling this while mallocing is set,
    // typically because this is called while panicing.
    // Crash reliably, rather than only when we need to grow
    // the hash table.
    if getg().m.mallocing != 0 {
        throw("malloc deadlock")
    }

    t := itabTable
    if t.count >= 3*(t.size/4) { // 75% load factor
        // Grow hash table.
        // t2 = new(itabTableType) + some additional entries
        // We lie and tell malloc we want pointer-free memory because
        // all the pointed-to values are not in the heap.
        t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
        t2.size = t.size * 2

        // Copy over entries.
        // Note: while copying, other threads may look for an itab and
        // fail to find it. That's ok, they will then try to get the itab lock
        // and as a consequence wait until this copying is complete.
        iterate_itabs(t2.add)
        if t2.count != t.count {
            throw("mismatched count during itab table copy")
        }
        // Publish new hash table. Use an atomic write: see comment in getitab.
        atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
        // Adopt the new table as our own. 
        t = itabTable
        // Note: the old table can be GC'ed here.
    }
    t.add(m)
}

// add adds the given itab to itab table t.
// itabLock must be held.
func (t *itabTableType) add(m *itab) {
    // See comment in find about the probe sequence.
    // Insert new itab in the first empty spot in the probe sequence.
    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 {
            // A given itab may be used in more than one module
            // and thanks to the way global symbol resolution works, the
            // pointed-to itab may already have been inserted into the
            // global 'hash'.
            return
        }
        if m2 == nil {
            // Use atomic write here so if a reader sees m, it also
            // sees the correctly initialized fields of m.
            // NoWB is ok because m is not in heap memory.
            // *p = m
            atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
            t.count++
            return
        }
        h += I
        h &= mask
    }
}

可以看到,當(dāng)hash表使用達(dá)到75%或以上時(shí),就會(huì)進(jìn)行擴(kuò)容,容量是原來(lái)的2倍,申請(qǐng)完空間,就會(huì)把老表中的數(shù)據(jù)插入到新的hash表中。然后使itabTable指向新的表,最后把新的itab插入到新表中。


2.4 類(lèi)型判斷

2.4.1 接口轉(zhuǎn)接口

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 {
        // explicit conversions require non-nil interface value.
        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
}

首先我們看到有兩種用法:

  1. 返回值是一個(gè)時(shí),不能轉(zhuǎn)換就panic。
  2. 返回值是兩個(gè)時(shí),第二個(gè)返回值標(biāo)記能否轉(zhuǎn)換成功

此外,data復(fù)制的是指針,不會(huì)完整拷貝值

2.4.2 接口轉(zhuǎn)具體類(lèi)型

接口判斷是否轉(zhuǎn)換成具體類(lèi)型,是編譯器生成好的代碼去做的。我們看個(gè)empty interface轉(zhuǎn)換成具體類(lèi)型的例子:

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.typ
LEAQ type.*+60128(SB), DX    //DX = &type.int
CMPQ DX, CX.                 //if DX == AX

可以看到empty interface轉(zhuǎn)具體類(lèi)型,是編譯器生成好對(duì)比代碼,比較具體類(lèi)型和空接口是不是同一個(gè)type,而不是調(diào)用某個(gè)函數(shù)在運(yùn)行時(shí)動(dòng)態(tài)對(duì)比。

然后我們?cè)倏聪路强战涌陬?lèi)型轉(zhuǎn)換:

var tf Tester
var t testStruct

func F4() int{
    t := tf.(testStruct)
    return t.i
}

func main() {
    F4()
}

繼續(xù)反匯編看一下:

MOVQ main.tf(SB), CX   // CX = tf.tab(.inter.typ)
LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對(duì)應(yīng)的&itab(.inter.typ)
CMPQ DX, CX //

可以看到,非空接口轉(zhuǎn)具體類(lèi)型,也是編譯器生成的代碼,比較是不是同一個(gè)itab,而不是調(diào)用某個(gè)函數(shù)在運(yùn)行時(shí)動(dòng)態(tài)對(duì)比。


最后

我還沒(méi)確定golang程序啟動(dòng)時(shí),是否會(huì)把編譯期生成的itab插入到全局的hash表中?
還有賦值給interface時(shí),編譯優(yōu)化避免malloc,這里我也不太懂
誰(shuí)知道可以告訴一下我。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容