首頁  >  文章  >  後端開發  >  go中的資料結構-介面interface(詳解)

go中的資料結構-介面interface(詳解)

angryTom
angryTom轉載
2019-11-28 15:13:114352瀏覽

go中的資料結構-介面interface(詳解)

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 e09be6022d700e04aeaa85a5f42fdcb2 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 = c1cf2bd5b63c974afcb2fded387850cf对应的&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 cb1d4a6c14c32b4c4b685b382806b485= 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语言教程

以上是go中的資料結構-介面interface(詳解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除
上一篇:go get命令詳解下一篇:go get命令詳解