Go語言Interface機制解析

Concurrency發表於2015-03-04

前幾日一朋友在學GO,問了我一些interface機制的問題。試著解釋發現自己也不是太清楚,所以今天下午特意查了資料和閱讀GO的原始碼(基於go1.4),整理出了此文。如果有錯誤的地方還望指正。

GO語言的interface是我比較喜歡的特性之一。interface與struct之間可以相互轉換,struct不需要像JAVA在原始碼中顯示說明實現了某個介面,可以通過約定的形式,隱式的轉換到interface,還可以在執行時查詢介面型別,這樣有種用動態語言寫程式碼的感覺,但是又可以在編譯時進行檢查,捕捉一些明顯的型別不匹配的錯誤。

type Stringer interface {
    String() string
}

type S struct {
     i int
}

func (s *S) String() string {
    return fmt.Sprintf("%d", s.i)
}

func Print(s Stringer) {
    println(s.String())
}

func DynamicPrint(any interface{}) {
   if s, ok := any.(Stringer); ok {
       Print(s)
   }
}

func main() {
   var s S
   s.i = 123456789
   Print(&s)
   DynamicPrint(&s)
}

如上面的程式碼所示,型別S沒有顯示的實現Stringer介面,但是它的方法列表符合Stringer介面,所以可以轉換為Stringer介面使用。

那麼,GO語言的interface機制到底是如何實現的呢?

interface value

上述程式碼中函式Print的引數是一個Stringer介面,也就是Stringer的一個物件例項。這個物件例項叫做interface value。它的資料結構如下:

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

其中tab欄位類似於C++的vptr,tab中包含了對應的方法陣列,除此之外還儲存了實現該介面的型別後設資料。data是對應的實現該介面的型別的例項指標。

itab資料結構如下:

type itab struct {
    inter     *interfacetype
    _type     *_type
    link      *itab
    bad       int32
    unused    int32
    fun       [0]unsafe.Pointer
}

其中inter欄位表示這個interface value所屬的介面元資訊,_type欄位表示具體實現型別的元資訊,fun欄位表示該interface的方法陣列。link,bad,unused欄位暫時不關心。

當我們在GO程式碼中呼叫一個介面的方法時,操作類似如下: s.tab->fun[0](s.data)。呼叫開銷還是很小的。

Itab的生成方式

一個自定義的結構體可以實現某個介面,然後可以隱式的轉換到對應的介面。這種操作有點像C++的派生類轉換為基類一樣,這個操作是一個執行時繫結過程。而GO語言的interface機制還有一些其他特性:比如一個具體型別可以實現N多方法,但是隻有其中某幾個或者全部都滿足某個介面,而此時,不可能把所有的方法都放到Itab中,這就意味著需要在繫結過程中剔除某些不需要的方法。

GO編譯器會在編譯時會為每個自定義結構體和interface型別生成一個型別後設資料,用來描述這個型別的名稱,型別的HASH值,型別的方法列表,方法列表中還包括了方法的名稱。而在一個自定義結構體轉換到一個interface型別時,GO編譯器會生成程式碼,使其在執行時計算Itab,完成動態繫結方法的需求。這個計算Itab的過程相對來說比較簡單,因為GO編譯器生成的型別後設資料中包含了所有的方法名稱和地址,那麼在一個結構體例項轉換為interface value時,只需要把interface的方法列表作為基,方法名和方法型別作為KEY,去結構體後設資料中查詢對應的方法即可。

GO的runtime庫中對Itab的查詢過程做了優化,由O(ni * nt)複雜度變為O(ni + nt)。依據是一個自定義結構體實現的方法一定是大於或等於某個具體interface的方法集的。所以可以事先把所有的方法按照名字從小到大排序,然後在匹配到一個方法後,可以在下次查詢時使用上次的索引值。

除此之外,GO編譯器為了減少每次不必要的Itab,還增加了一個對應的itab的快取。你可以編譯一個GO程式,然後反編譯後可以檢視到一個類似go_itab__main_S_main_Stringer名稱的變數。在每次一個結構體轉換到一個interface之前都會檢查這個快取是否有效,有效就使用。這個檢查也只是一個cmp指令而已。

還有在GO執行時庫裡,為了減少每次的Itab實現,還做了相應的優化。內部實現了一個HASH表,儲存了每個具體結構體到interface轉換生成的Itab例項。程式碼可以在go\src\runtime\iface.go getitab函式中看到。

interface{}的特殊處理

interface{}在GO中是一個特殊的內建型別,類似於C/C++中的void*,但是包含了型別資訊。所以你可以把任意的資料轉換到interface{},然後通過type assert從interface{}獲取原有的資料。但是正如你所見,interface{}沒有方法,那麼也就是說,它不需要iface中的itab,因為不需要方法繫結。針對此,做了特殊修改,iface中的tab欄位型別由itab指標變為了對應的具體實現型別的型別後設資料指標。在GO原始碼中,interface{}物件的型別原型如下:

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

eface是empty interface的縮寫。

其他

在GO的原始碼iface.go中,還可以看到很多函式比如叫assertE2E,assertE2I,assertE2T等,這些函式就是對應的type assert的具體實現函式。E表示eface,I表示iface,T表示自定義的結構體或者基於內建型別創造出的型別。程式碼都比較簡單,不在敘述了。

總結

想理解interface機制的實現,只需要理解型別後設資料以及動態繫結過程。其中要還區分interface value,也就是內部的iface結構體。因此引出了Itable的概念。整體來說不是太複雜,資料結構也比較簡單,如果你有時間的話,也可以自己看下GO的原始碼。

參考

GO原始碼(go\src\runtime\iface.go)《Go Data Structures: Interfaces》《Go Interfaces

相關文章