1. 程式人生 > >Go語言中interface

Go語言中interface

interface是Go語言中最成功的設計之一,空的interface可以被當作“鴨子”型別使用,它使得Go這樣的靜態語言擁有了一定的動態性,但卻又不損失靜態語言在型別安全方面擁有的編譯時檢查的優勢。
依賴於介面而不是實現,優先使用組合而不是繼承,這是程式抽象的基本原則。但是長久以來以C++為代表的“面向物件”語言曲解了這些原則,讓人們走入了誤區。為什麼要將方法和資料綁死?為什麼要有多重繼承這麼變態的設計?面向物件中最強調的應該是物件間的訊息傳遞,卻為什麼被演繹成了封裝繼承和多型。面向物件是否實現程式程式抽象的合理途徑,又或者是因為它存在我們就認為它合理了。歷史原因,中間出現了太多的錯誤。不管怎麼樣,Go的interface給我們打開了一扇新的窗。
那麼,Go中的interface在底層是如何實現的呢?
Eface和Iface
interface實際上就是一個結構體,包含兩個成員。其中一個成員是指向具體資料的指標,另一個成員中包含了型別資訊。空介面和帶方法的介面略有不同,下面分別是空介面和帶方法的介面是使用的資料結構:
struct Eface
{
Type* type;
void* data;
};
struct Iface
{
Itab* tab;
void* data;
};
先看Eface,它是interface{}底層使用的資料結構。資料域中包含了一個void指標,和一個型別結構體的指標。interface{}扮演的角色跟C語言中的void

是差不多的,Go中的任何物件都可以表示為interface{}。不同之處在於,interface{}中有型別資訊,於是可以實現反射。
型別資訊的結構體定義如下:
struct Type
{
uintptr size;
uint32 hash;
uint8 _unused;
uint8 align;
uint8 fieldAlign;
uint8 kind;
Alg *alg;
void *gc;
String string;
UncommonType x;
Type ptrto;
};
其實在前面我們已經見過它了。精確的垃圾回收中,就是依賴Type結構體中的gc域的。不同型別資料的型別資訊結構體並不完全一致,Type是型別資訊結構體中公共的部分,其中size描述型別的大小,hash資料的hash值,align是對齊,fieldAlgin是這個資料嵌入結構體時的對齊,kind是一個列舉值,每種型別對應了一個編號。alg是一個函式指標的陣列,儲存了hash/equal/print/copy四個函式操作。UncommonType是指向一個函式指標的陣列,收集了這個型別的實現的所有方法。
在reflect包中有個KindOf函式,返回一個interface{}的Type,其實該函式就是簡單的取Eface中的Type域。
Iface和Eface略有不同,它是帶方法的interface底層使用的資料結構。data域同樣是指向原始資料的,而Itab的結構如下:
struct Itab
{
InterfaceType
inter;
Type
type;
Itab
link;
int32 bad;
int32 unused;
void (*fun[])(void);
};
Itab中不僅儲存了Type資訊,而且還多了一個方法表fun[]。一個Iface中的具體型別中實現的方法會被拷貝到Itab的fun陣列中。
具體型別向介面型別賦值
將具體型別資料賦值給interface{}這樣的抽象型別,中間會涉及到型別轉換操作。從介面型別轉換為具體型別(也就是反射),也涉及到了型別轉換。這個轉換過程中做了哪些操作呢?先看將具體型別轉換為介面型別。如果是轉換成空介面,這個過程比較簡單,就是返回一個Eface,將Eface中的data指標指向原型資料,type指標會指向資料的Type結構體。
將某個型別資料轉換為帶方法的介面時,會複雜一些。中間涉及了一道檢測,該型別必須要實現了介面中宣告的所有方法才可以進行轉換。這個檢測是在編譯過程中做的,我們可以做個測試:
type I interface {
String()
}
var a int = 5
var b I = a
編譯會報錯:
cannot use a (type int) as type I in assignment:
int does not implement I (missing String method)
說明具體型別轉換為帶方法的介面型別是在編譯過程中進行檢測的。
那麼這個檢測是如何實現的呢?在runtime下找到了iface.c檔案,應該是早期版本是在執行時檢測留下的,其中有一個itab函式就是判斷某個型別是否實現了某個介面,如果是則返回一個Itab結構體。
型別轉換時的檢測就是比較具體型別的方法表和介面型別的方法表,看具體型別是實現了介面型別所宣告的所有的方法。還記得Type結構體中是有個UncommonType欄位的,裡面有張方法表,型別所實現的方法都在裡面。而在Itab中有個InterfaceType欄位,這個欄位中也有一張方法表,就是這個介面所要求的方法。這兩處方法表都是排序過的,只需要一遍順序掃描進行比較,應該可以知道Type中否實現了介面中宣告的所有方法。最後還會將Type方法表中的函式指標,拷貝到Itab的fun欄位中。
這裡提到了三個方法表,有點容易把人搞暈,所以要解釋一下。
Type的UncommonType中有一個方法表,某個具體型別實現的所有方法都會被收集到這張表中。reflect包中的Method和MethodByName方法都是通過查詢這張表實現的。表中的每一項是一個Method,其資料結構如下:
struct Method
{
String *name;
String *pkgPath;
Type *mtyp;
Type *typ;
void (*ifn)(void);
void (*tfn)(void);
};
Iface的Itab的InterfaceType中也有一張方法表,這張方法表中是介面所宣告的方法。其中每一項是一個IMethod,資料結構如下:
struct IMethod
{
String *name;
String *pkgPath;
Type *type;
};
跟上面的Method結構體對比可以發現,這裡是只有宣告沒有實現的。
Iface中的Itab的func域也是一張方法表,這張表中的每一項就是一個函式指標,也就是隻有實現沒有宣告。
型別轉換時的檢測就是看Type中的方法表是否包含了InterfaceType的方法表中的所有方法,並把Type方法表中的實現部分拷到Itab的func那張表中。
reflect
reflect就是給定一個介面型別的資料,得到它的具體型別的型別資訊,它的Value等。reflect包中的TypeOf和ValueOf函式分別做這個事情。
還有像

v, ok := i.(T)

這樣的語法,也是判斷一個介面i的具體型別是否為型別T,如果是則將其值返回給v。這跟上面的型別轉換一樣,也會檢測轉換是否合法。不過這裡的檢測是在執行時執行的。在runtime下的iface.c檔案中,有一系統的assetX2X函式,比如runtime.assetE2T,runtime.assetI2T等等。這個實現起來比較簡單,只需要比較Iface中的Itab的type是否與給定Type為同一個。