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