1. 程式人生 > >淺析 golang interface 實現原理

淺析 golang interface 實現原理

假設 upload 數據 地址 nsh 指針 target 讓我 dia

interface 在 golang 中是一個非常重要的特性。它相對於其它語言有很多優勢:

  1. duck typing。大多數的靜態語言需要顯示的聲明類型的繼承關系。而 golang 通過 interface 實現了?duck typing, 使得我們無需顯示的類型繼承。
  2. 不像其它實現了?duck typing?的動態語言那樣,只能在運行時才能檢查到類型的轉換錯誤。而 golang 的 interface 特性可以讓我們在編譯時就能發現錯誤。

本文將簡單分析 interface 的實現原理。


interface 的數據結構

eface 和 iface

技術分享圖片

  • eface?表示空的?interface{}
    ,它用兩個機器字長表示,第一個字 _type 是指向實際類型描述的指針,第二個字 data 代表數據指針。
  • iface?表示至少帶有一個函數的 interface, 它也用兩個機器字長表示,第一個字 tab 指向一個 itab 結構,第二個字 data 代表數據指針。

data

data 用來保存實際變量的地址。

data 中的內容會根據實際情況變化,因為 golang 在函數傳參和賦值時是?值傳遞?的,所以:

  1. 如果實際類型是一個值,那麽 interface 會保存這個值的一份拷貝。interface 會在堆上為這個值分配一塊內存,然後 data 指向它。
  2. 如果實際類型是一個指針,那麽 interface 會保存這個指針的一份拷貝。由於 data 的長度恰好能保存這個指針的內容,所以 data 中存儲的就是指針的值。它和實際數據指向的是同一個變量。

以 interface{} 的賦值為例:

技術分享圖片

上圖中, i1 和 i2 是 interface,A 為要賦值給 interface 的對象。

  • i1 = A?將 A 的值賦值給 i1,則 i1 中的 data 中的內容是一塊新內存的地址 (0x123456),這塊內存的值從 A 拷貝。
  • i2 = &A?將 A 的地址賦值給 i2,則 i2 中的 data 的值為 A 的地址,即 0xabcdef;

itab

技術分享圖片

itab 表示 interface 和 實際類型的轉換信息。對於每個 interface 和實際類型,只要在代碼中存在引用關系, go 就會在運行時為這一對具體的 <Interface, Type> 生成 itab 信息。

  • inter 指向對應的 interface 的類型信息。
  • type 和 eface 中的一樣,指向的是實際類型的描述信息 _type
  • fun 為函數列表,表示對於該特定的實際類型而言,interface 中所有函數的地址。

_type

技術分享圖片

_type 表示類型信息。每個類型的 _type 信息由編譯器在編譯時生成。其中:

  • size 為該類型所占用的字節數量。
  • kind 表示類型的種類,如 bool、int、float、string、struct、interface 等。
  • str 表示類型的名字信息,它是一個 nameOff(int32) 類型,通過這個 nameOff,可以找到類型的名字字符串
  • 灰色的 extras 對於基礎類型(如 bool,int, float 等)是 size 為 0 的,它為復雜的類型提供了一些額外信息。例如為 struct 類型提供 structtype,為 slice 類型提供 slicetype 等信息。
  • 灰色的 ucom 對於基礎類型也是 size 為 0 的,但是對於?type Binary int?這種定義或者是其它復雜類型來說,ucom 用來存儲類型的函數列表等信息。
  • 註意 extras 和 ucom 的圓頭箭頭,它表示 extras 和 ucom 不是指針,它們的內容位於 _type 的內存空間中。

interfacetype

技術分享圖片

interfacetype 也並沒有什麽神奇的地方,只是 _type 為 interface 類型提供的另一種信息罷了。 它包括這個 interface 所申明的所有函數信息。


interface 相關的操作

itab 中函數表(fun) 的生成

假設 interface 有 ni 個函數, struct 有 nt 個函數,那麽 itab 中的函數表生成的時間復雜度為 O(ni*nt) (遍歷 interface 的所有函數,對每次叠代都從 struct 中遍歷找到匹配的函數)。 但實際上編譯器對此做了優化,它將 interfacetype 中的函數列表和 uncommontype 中的函數列表都做了排序. 所以實現了 O(ni+nt) 時間復雜度的算法。

// 生成 itab 的 funcs 的算法
// 代碼摘錄自 $GOROOT/src/runtime/iface.go
// 經過了部分修改,只保留了最核心的邏輯

var j = 0
for k := 0; k < ni; k++ {
    mi := inter.methods[k]
    for ; j < nt; j++ {
        mt := t.methods[j]
        if isOk(mi, mt) {
            itab.fun[k] = mt.f
        }
    }
}

interface 參數傳遞與函數調用

type Binary uint64

func (i Binary) String() string {
    return strconv.FormatUint(uint64(i), 10)
}

type Stringer interface {
    String() string
}

func test(s Stringer) {
    s.String()
}

func main() {
    b := Binary(0x123)
    test(b)
}

在上面的代碼中,golang 的參數傳遞過程是:

  1. 分配一塊內存 p, 並且將對象 b 的內容拷貝到 p 中;
  2. 創建 iface 對象 i,將 i.tab 賦值為 itab<Stringer, Binary>。將 i.data 賦值為 p;
  3. 使用 i 作為參數調用 test 函數。

當 test 函數執行 s.String 時,實際上就是在 s.tab 的 fun 中索引(索引由編譯器在編譯時生成)到 String 函數,並且調用它。


參考資料:

  1. Why I like Go’s interfaces
  2. duck typing
  3. Go Data Structures: Interfaces
  4. go 源碼?https://github.com/golang/go/tree/master/src/runtime

本文始發於https://zhuanlan.zhihu.com/p/60983066

淺析 golang interface 實現原理