go-淺談接口實現原理


淺析 golang interface 實現原理

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

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

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


interface 的數據結構

eface 和 iface

img

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

data

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

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

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

以 interface{} 的賦值為例:

img

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

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

itab

img

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

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

_type

img

_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

img

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 函數,並且調用它。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM