【Go進階—基礎特性】介面

與昊發表於2022-03-13

介面定義了一種規範,描述了類的行為和功能。我們都知道,Go 語言中的介面是所謂的 Duck Typing,實現介面的所有方法也就隱式地實現了介面,那麼,它是怎麼實現的呢?

資料結構

在 Go 語言中,介面分為兩類:

  • eface:用於表示沒有方法的空介面型別變數,即 interface{} 型別的變數。
  • iface:用於表示其餘擁有方法的介面型別變數。

eface

eface 的資料結構如下:

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

eface 有兩個屬性,分別是 _type 和 data,分別指向介面變數的動態型別和動態值。

再進一步看看 type 屬性的結構:

type _type struct {
    size       uintptr // 型別大小
    ptrdata    uintptr // 包含所有指標的記憶體字首的大小
    hash       uint32  // 型別的 hash 值
    tflag      tflag   // 型別的 flag 標誌,主要用於反射
    align      uint8   // 記憶體對齊相關
    fieldAlign uint8   // 記憶體對齊相關
    kind       uint8   // 型別的編號,包含 Go 語言中的所有型別,如 kindBool、kindInt 等
    equal func(unsafe.Pointer, unsafe.Pointer) bool // 用於比較此物件的回撥函式
    gcdata    *byte    // 儲存垃圾收集器的 GC 型別資料
    str       nameOff 
    ptrToThis typeOff
}

注:Go 語言的各種資料型別都是在 _type 欄位的基礎上,增加一些額外的欄位來進行管理的。

來看一個 eface 變數的例子:

type T struct {
    n int
    s string
}

func main() {
    var t = T {
        n: 17,
        s: "hello, interface",
    }
    var ei interface{} = t
    println(ei)
}           
            

ei 變數的結構對應於下圖:

image.png

iface

iface 的結構如下:

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

與 eface 結構體一樣,iface 儲存的也是型別和值資訊,不過因為 iface 還要儲存介面本身的資訊以及動態型別所實現的方法的資訊,因此 iface 稍顯複雜,它的第一個欄位指向一個 itab 型別結構:

type itab struct {
    inter *interfacetype // 介面的型別資訊
    _type *_type         // 動態型別資訊
    hash  uint32         // _type.hash 的副本,當我們想將 interface 型別轉換成具體型別時,可以使用該欄位快速判斷目標型別和具體型別 _type 是否一致
    _     [4]byte    
    fun   [1]uintptr     // 儲存介面方法集的具體實現的地址,包含一組函式指標,實現了介面方法的動態分派,且每次在介面發生變更時都會更新
}

進一步展開 interfacetype 結構體。原始碼如下:

type nameOff int32
type typeOff int32

type imethod struct {
    name nameOff
    ityp typeOff
}

type interfacetype struct {
    typ     _type     // 動態型別資訊
    pkgpath name      // 包名資訊
    mhdr    []imethod // 介面所定義的方法列表
}

iface 的示例如下:

type T struct {
    n int
    s string
}

func (T) M1() {}
func (T) M2() {}

type NonEmptyInterface interface {
    M1()
    M2()
}

func main() {
    var t = T{
        n: 18,
        s: "hello, interface",
    }
    var i NonEmptyInterface = t
    println(i)
}            

變數 i 對應如下:

image.png

值接收者和指標接收者

在使用 Go 語言的過程中,在呼叫方法的時候,不管方法的接收者是什麼型別,該型別的值和指標都可以呼叫,不必嚴格符合接收者的型別。

需要記住的一點是:在 Go 語言中,如果實現了接收者是值型別的方法,會隱含實現接收者是指標型別的方法,反之則不成立。之所以可以使用值型別呼叫指標型別的方法,是語法糖的作用。如果只有指標型別實現了介面,使用值型別呼叫介面方法則會報錯。

介面值的比較

我們看到,所有的介面型別其實底層都包含兩個欄位:型別和值,也被稱為動態型別和動態值。因此介面值包括動態型別和動態值,在比較介面值的時候,我們需要分別對介面值的型別和值進行比較。

nil 介面變數

package main

func main() {
    var i interface{}
    var err error
    println(i)
    println(err)
    println("i = nil:", i == nil)
    println("err = nil:", err == nil)
    println("i = err:", i == err)
    println("")
}

// 輸出結果

(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true

我們看到,無論是空介面型別變數還是非空介面型別變數,一旦變數值為 nil,那麼它們內部表示均為(0x0,0x0),即型別資訊和資料資訊均為空。因此上面的變數 i 和 err 等值判斷為 true。

空介面型別變數

func main() {
    var eif1 interface{}
    var eif2 interface{}
    n, m := 17, 18

    eif1 = n
    eif2 = m

    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2)

    eif2 = 17
    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2)

    eif2 = int64(17)
    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2)
}

// 輸出結果

eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0xc00007ef40)
eif1 = eif2: false
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0x10eb3d0)
eif1 = eif2: true
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac640,0x10eb3d8)
eif1 = eif2: false

從輸出結果可以看到:對於空介面型別變數,只有在 _type 和 data 所指資料內容一致(不是資料指標的值一致)的情況下,兩個空介面型別變數才相等。

Go 在建立 eface 時一般會為 data 重新分配記憶體空間,將動態型別變數的值複製到這塊記憶體空間,並將 data 指標指向這塊記憶體空間。因此我們在多數情況下看到的 data 指標值是不同的。但 Go 對於 data 的分配是有優化的,也不是每次都分配新記憶體空間,就像上面的 eif2 的 0x10eb3d0 和 0x10eb3d8 兩個 data 指標值,顯然是直接指向了一塊事先建立好的靜態資料區。

非空介面型別變數

func main() {
    var err1 error
    var err2 error
    err1 = (*T)(nil)
    println("err1:", err1)
    println("err1 = nil:", err1 == nil)

    err1 = T(5)
    err2 = T(6)
    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)

    err2 = fmt.Errorf("%d\n", 5)
    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)
}

// 輸出結果

err1: (0x10ed120,0x0)
err1 = nil: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed1a0,0x10eb318)
err1 = err2: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed0c0,0xc000010050)
err1 = err2: false            

與空介面型別變數一樣,只有在 tab 和 data 所指資料內容一致的情況下,兩個非空介面型別變數之間才能畫等號。

空介面型別變數與非空介面型別變數

func main() {
    var eif interface{} = T(5)
    var err error = T(5)
    println("eif:", eif)
    println("err:", err)
    println("eif = err:", eif == err)

    err = T(6)
    println("eif:", eif)
    println("err:", err)
    println("eif = err:", eif == err)
}

// 輸出結果

eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false                 

空介面型別變數和非空介面型別變數內部表示的結構有所不同,似乎一定不能相等。但 Go 在進行等值比較時,型別比較使用的是 eface 的 _type 和 iface 的 tab._type,因此就像我們在這個例子中看到的那樣,當 eif 和 err 都被賦值為 T(5) 時,兩者之間是相等的。

型別轉換

常規變數轉換介面變數

先看程式碼示例:

import "fmt"

type T struct {
    n int
    s string
}

func (T) M1() {}
func (T) M2() {}

type NonEmptyInterface interface {
    M1()
    M2()
}

func main() {
    var t = T{
        n: 17,
        s: "hello, interface",
    }
    var ei interface{}
    ei = t

    var i NonEmptyInterface
    i = t
    fmt.Println(ei)
    fmt.Println(i)
}

使用 go tool compile -S 命令檢視生成的彙編程式碼,可以看到這兩個轉換過程對應了 runtime 包的兩個函式:

......
0x0050 00080 (main.go:24)       CALL    runtime.convT2E(SB)
......
0x0089 00137 (main.go:27)       CALL    runtime.convT2I(SB)
......

這兩個函式的原始碼如下:

// $GOROOT/src/runtime/iface.go
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)
    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
}                        

convT2E 用於將任意型別轉換為一個 eface,convT2I 用於將任意型別轉換為一個 iface。兩個函式的實現邏輯相似,主要思路就是根據傳入的型別資訊(convT2E 的 _type 和 convT2I 的 tab._type)分配一塊記憶體空間,並將 elem 指向的資料複製到這塊記憶體空間中,最後傳入的型別資訊作為返回值結構中的型別資訊,返回值結構中的資料指標指向新分配的那塊記憶體空間。

那麼 convT2E 和 convT2I 函式的型別資訊從何而來?這些都依賴 Go 編譯器的工作。Go 也在不斷轉換操作進行優化,包括對常見型別(如整型、字串、切片等)提供一系列快速轉換函式:

// $GOROOT/src/cmd/compile/internal/gc/builtin/runtime.go
func convT16(val any) unsafe.Pointer     // val必須是一個 uint-16 相關型別的引數
func convT32(val any) unsafe.Pointer     // val必須是一個 unit-32 相關型別的引數
func convT64(val any) unsafe.Pointer     // val必須是一個 unit-64 相關型別的引數
func convTstring(val any) unsafe.Pointer // val必須是一個字串型別的引數
func convTslice(val any) unsafe.Pointer  // val必須是一個切片型別的引數                        

編譯器知道每個要轉換為介面型別變數的動態型別變數的型別,會根據這一型別選擇適當的 convT2X 函式。

介面變數互相轉換

介面之間互相轉換的前提是型別相容,也就是都實現了介面定義的方法。下面我們來看一下執行時轉換介面型別的方法:

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
}

程式碼比較簡單,函式引數 inter 表示介面型別,i 表示繫結了動態型別的介面變數,返回值 r 就是需要轉換的新的 iface。通過前面的分析,我們知道 iface 是由 tab 和 data 兩個欄位組成。所以,convI2I 函式真正要做的事就是找到並設定好新 iface 的 tab 和 data,就大功告成了。

我們還知道,tab 是由介面型別 interfacetype 和 實體型別 _type 組成的。所以最關鍵的語句是 r.tab = getitab(inter, tab._type, false),來看一下 getitab 的核心程式碼:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    var m *itab

    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.hash = 0
    m.init()
    itabAdd(m)
    unlock(&itabLock)
finish:
    if m.fun[0] != 0 {
        return m
    }
    if canfail {
        return nil
    }

    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
  • 呼叫 atomic.Loadp 方法載入並查詢現有的 itab hash table,看看是否是否可以找到所需的 itab 元素。
  • 若沒有找到,則呼叫 lock 方法對 itabLock 上鎖,並再查詢一次。

    • 若找到,則跳到 finish 標識的收尾步驟。
    • 若沒有找到,則新生成一個 itab 元素,並呼叫 itabAdd 方法新增到全域性的 hash table 中。
  • 返回所需的 itab。

相關文章