淺析 golang interface 實現原理
interface 在 golang 中是一個非常重要的特性。它相對於其它語言有很多優勢:
- duck typing。大多數的靜態語言需要顯示的聲明類型的繼承關系。而 golang 通過 interface 實現了?
duck typing
, 使得我們無需顯示的類型繼承。 - 不像其它實現了?
duck typing
?的動態語言那樣,只能在運行時才能檢查到類型的轉換錯誤。而 golang 的 interface 特性可以讓我們在編譯時就能發現錯誤。
本文將簡單分析 interface 的實現原理。
interface 的數據結構
eface 和 iface
eface
?表示空的?interface{}
iface
?表示至少帶有一個函數的 interface, 它也用兩個機器字長表示,第一個字 tab 指向一個 itab 結構,第二個字 data 代表數據指針。
data
data 用來保存實際變量的地址。
data 中的內容會根據實際情況變化,因為 golang 在函數傳參和賦值時是?值傳遞
?的,所以:
- 如果實際類型是一個值,那麽 interface 會保存這個值的一份拷貝。interface 會在堆上為這個值分配一塊內存,然後 data 指向它。
- 如果實際類型是一個指針,那麽 interface 會保存這個指針的一份拷貝。由於 data 的長度恰好能保存這個指針的內容,所以 data 中存儲的就是指針的值。它和實際數據指向的是同一個變量。
以 interface{} 的賦值為例:
上圖中, i1 和 i2 是 interface,A 為要賦值給 interface 的對象。
- i1 = A?將 A 的值賦值給 i1,則 i1 中的 data 中的內容是一塊新內存的地址 (0x123456),這塊內存的值從 A 拷貝。
- i2 = &A?將 A 的地址賦值給 i2,則 i2 中的 data 的值為 A 的地址,即 0xabcdef;
itab
itab 表示 interface 和 實際類型的轉換信息。對於每個 interface 和實際類型,只要在代碼中存在引用關系, go 就會在運行時為這一對具體的 <Interface, Type> 生成 itab 信息。
- inter 指向對應的 interface 的類型信息。
- type 和 eface 中的一樣,指向的是實際類型的描述信息 _type
- fun 為函數列表,表示對於該特定的實際類型而言,interface 中所有函數的地址。
_type
_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
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 的參數傳遞過程是:
- 分配一塊內存 p, 並且將對象 b 的內容拷貝到 p 中;
- 創建 iface 對象 i,將 i.tab 賦值為 itab<Stringer, Binary>。將 i.data 賦值為 p;
- 使用 i 作為參數調用 test 函數。
當 test 函數執行 s.String 時,實際上就是在 s.tab 的 fun 中索引(索引由編譯器在編譯時生成)到 String 函數,並且調用它。
參考資料:
- Why I like Go’s interfaces
- duck typing
- Go Data Structures: Interfaces
- go 源碼?https://github.com/golang/go/tree/master/src/runtime
本文始發於https://zhuanlan.zhihu.com/p/60983066
淺析 golang interface 實現原理