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》
相關文章
- Go語言interface底層實現Go
- Go語言錯誤處理機制Go
- 用 Go 語言實作 Job Queue 機制Go
- Go語言處理—Day11—反射機制Go反射
- Golang | Go語言多型的實現與interface使用Golang多型
- go語言遊戲服務端開發(四)——RPC機制Go遊戲服務端RPC
- 用Go語言異常機制模擬TryCatch異常捕捉Go
- Go 語言解析 yaml 檔案的方法GoYAML
- Go語言最新面試題及其解析Go面試題
- go語言Json解析實用工具 - gjsonGoJSON
- go語言遊戲服務端開發(三)——服務機制Go遊戲服務端
- Go語言————1、初識GO語言Go
- Go語言最新面試題及其解析(一)Go面試題
- Go 語言實現解析器翻譯Go
- go語言請求http介面示例 並解析jsonGoHTTPJSON
- GO語言————2、GO語言環境安裝Go
- ARM下C語言棧幀機制C語言
- 為什麼在Go語言中要慎用interface{}Go
- 【Go語言入門系列】(八)Go語言是不是面嚮物件語言?Go物件
- Go_go語言初探Go
- 深入理解GO語言之併發機制Go
- go 語言常量Go
- go語言使用Go
- Go語言mapGo
- go 語言切片Go
- Go 語言的詞法分析和語法分析(2)—Import宣告的解析Go詞法分析語法分析Import
- 什麼是Go語言?Go語言有什麼特點?Go
- go語言與c語言的相互呼叫GoC語言
- 【譯】Go語言宣告語法Go
- go語言學習Go
- 初識go語言Go
- GO語言併發Go
- Go 語言效能分析Go
- Go語言的”坑“Go
- go 語言陣列Go陣列
- Go 語言函式Go函式
- Go 語言運算子Go
- Go語言簡史Go
- go語言的介面Go