歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

淺析Go語言的Interface機制

前幾日一朋友在學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》

Linux系統入門學習-在Linux中安裝Go語言  http://www.linuxidc.com/Linux/2015-02/113159.htm

Ubuntu 安裝Go語言包 http://www.linuxidc.com/Linux/2013-05/85171.htm

《Go語言編程》高清完整版電子書 http://www.linuxidc.com/Linux/2013-05/84709.htm

Go語言並行之美 -- 超越 “Hello World” http://www.linuxidc.com/Linux/2013-05/83697.htm

我為什麼喜歡Go語言 http://www.linuxidc.com/Linux/2013-05/84060.htm

Go語言內存分配器的實現 http://www.linuxidc.com/Linux/2014-01/94766.htm

Copyright © Linux教程網 All Rights Reserved