interface是Go語言中最成功的設(shè)計之一,空的interface可以被當作“鴨子”類型使用,它使得Go這樣的靜態(tài)語言擁有了一定的動態(tài)性,但卻又不損失靜態(tài)語言在類型安全方面擁有的編譯時檢查的優(yōu)勢。
依賴于接口而不是實現(xiàn),優(yōu)先使用組合而不是繼承,這是程序抽象的基本原則。但是長久以來以C++為代表的“面向?qū)ο蟆闭Z言曲解了這些原則,讓人們走入了誤區(qū)。為什么要將方法和數(shù)據(jù)綁死?為什么要有多重繼承這么變態(tài)的設(shè)計?面向?qū)ο笾凶顝娬{(diào)的應(yīng)該是對象間的消息傳遞,卻為什么被演繹成了封裝繼承和多態(tài)。面向?qū)ο笫欠駥崿F(xiàn)程序程序抽象的合理途徑,又或者是因為它存在我們就認為它合理了。歷史原因,中間出現(xiàn)了太多的錯誤。不管怎么樣,Go的interface給我們打開了一扇新的窗。
那么,Go中的interface在底層是如何實現(xiàn)的呢?
interface實際上就是一個結(jié)構(gòu)體,包含兩個成員。其中一個成員是指向具體數(shù)據(jù)的指針,另一個成員中包含了類型信息??战涌诤蛶Х椒ǖ慕涌诼杂胁煌?,下面分別是空接口和帶方法的接口是使用的數(shù)據(jù)結(jié)構(gòu):
struct Eface
{
Type* type;
void* data;
};
struct Iface
{
Itab* tab;
void* data;
};
先看Eface,它是interface{}底層使用的數(shù)據(jù)結(jié)構(gòu)。數(shù)據(jù)域中包含了一個void*指針,和一個類型結(jié)構(gòu)體的指針。interface{}扮演的角色跟C語言中的void*是差不多的,Go中的任何對象都可以表示為interface{}。不同之處在于,interface{}中有類型信息,于是可以實現(xiàn)反射。
類型信息的結(jié)構(gòu)體定義如下:
struct Type
{
uintptr size;
uint32 hash;
uint8 _unused;
uint8 align;
uint8 fieldAlign;
uint8 kind;
Alg *alg;
void *gc;
String *string;
UncommonType *x;
Type *ptrto;
};
其實在前面我們已經(jīng)見過它了。精確的垃圾回收中,就是依賴Type結(jié)構(gòu)體中的gc域的。不同類型數(shù)據(jù)的類型信息結(jié)構(gòu)體并不完全一致,Type是類型信息結(jié)構(gòu)體中公共的部分,其中size描述類型的大小,hash數(shù)據(jù)的hash值,align是對齊,fieldAlgin是這個數(shù)據(jù)嵌入結(jié)構(gòu)體時的對齊,kind是一個枚舉值,每種類型對應(yīng)了一個編號。alg是一個函數(shù)指針的數(shù)組,存儲了hash/equal/print/copy四個函數(shù)操作。UncommonType是指向一個函數(shù)指針的數(shù)組,收集了這個類型的實現(xiàn)的所有方法。
在reflect包中有個KindOf函數(shù),返回一個interface{}的Type,其實該函數(shù)就是簡單的取Eface中的Type域。
Iface和Eface略有不同,它是帶方法的interface底層使用的數(shù)據(jù)結(jié)構(gòu)。data域同樣是指向原始數(shù)據(jù)的,而Itab的結(jié)構(gòu)如下:
struct Itab
{
InterfaceType* inter;
Type* type;
Itab* link;
int32 bad;
int32 unused;
void (*fun[])(void);
};
Itab中不僅存儲了Type信息,而且還多了一個方法表fun[]。一個Iface中的具體類型中實現(xiàn)的方法會被拷貝到Itab的fun數(shù)組中。
將具體類型數(shù)據(jù)賦值給interface{}這樣的抽象類型,中間會涉及到類型轉(zhuǎn)換操作。從接口類型轉(zhuǎn)換為具體類型(也就是反射),也涉及到了類型轉(zhuǎn)換。這個轉(zhuǎn)換過程中做了哪些操作呢?先看將具體類型轉(zhuǎn)換為接口類型。如果是轉(zhuǎn)換成空接口,這個過程比較簡單,就是返回一個Eface,將Eface中的data指針指向原型數(shù)據(jù),type指針會指向數(shù)據(jù)的Type結(jié)構(gòu)體。
將某個類型數(shù)據(jù)轉(zhuǎn)換為帶方法的接口時,會復(fù)雜一些。中間涉及了一道檢測,該類型必須要實現(xiàn)了接口中聲明的所有方法才可以進行轉(zhuǎn)換。這個檢測是在編譯過程中做的,我們可以做個測試:
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)
說明具體類型轉(zhuǎn)換為帶方法的接口類型是在編譯過程中進行檢測的。
那么這個檢測是如何實現(xiàn)的呢?在runtime下找到了iface.c文件,應(yīng)該是早期版本是在運行時檢測留下的,其中有一個itab函數(shù)就是判斷某個類型是否實現(xiàn)了某個接口,如果是則返回一個Itab結(jié)構(gòu)體。
類型轉(zhuǎn)換時的檢測就是比較具體類型的方法表和接口類型的方法表,看具體類型是實現(xiàn)了接口類型所聲明的所有的方法。還記得Type結(jié)構(gòu)體中是有個UncommonType字段的,里面有張方法表,類型所實現(xiàn)的方法都在里面。而在Itab中有個InterfaceType字段,這個字段中也有一張方法表,就是這個接口所要求的方法。這兩處方法表都是排序過的,只需要一遍順序掃描進行比較,應(yīng)該可以知道Type中否實現(xiàn)了接口中聲明的所有方法。最后還會將Type方法表中的函數(shù)指針,拷貝到Itab的fun字段中。
這里提到了三個方法表,有點容易把人搞暈,所以要解釋一下。
Type的UncommonType中有一個方法表,某個具體類型實現(xiàn)的所有方法都會被收集到這張表中。reflect包中的Method和MethodByName方法都是通過查詢這張表實現(xiàn)的。表中的每一項是一個Method,其數(shù)據(jù)結(jié)構(gòu)如下:
struct Method
{
String *name;
String *pkgPath;
Type *mtyp;
Type *typ;
void (*ifn)(void);
void (*tfn)(void);
};
Iface的Itab的InterfaceType中也有一張方法表,這張方法表中是接口所聲明的方法。其中每一項是一個IMethod,數(shù)據(jù)結(jié)構(gòu)如下:
struct IMethod
{
String *name;
String *pkgPath;
Type *type;
};
跟上面的Method結(jié)構(gòu)體對比可以發(fā)現(xiàn),這里是只有聲明沒有實現(xiàn)的。
Iface中的Itab的func域也是一張方法表,這張表中的每一項就是一個函數(shù)指針,也就是只有實現(xiàn)沒有聲明。
類型轉(zhuǎn)換時的檢測就是看Type中的方法表是否包含了InterfaceType的方法表中的所有方法,并把Type方法表中的實現(xiàn)部分拷到Itab的func那張表中。
reflect就是給定一個接口類型的數(shù)據(jù),得到它的具體類型的類型信息,它的Value等。reflect包中的TypeOf和ValueOf函數(shù)分別做這個事情。
還有像
v, ok := i.(T)
這樣的語法,也是判斷一個接口i的具體類型是否為類型T,如果是則將其值返回給v。這跟上面的類型轉(zhuǎn)換一樣,也會檢測轉(zhuǎn)換是否合法。不過這里的檢測是在運行時執(zhí)行的。在runtime下的iface.c文件中,有一系統(tǒng)的assetX2X函數(shù),比如runtime.assetE2T,runtime.assetI2T等等。這個實現(xiàn)起來比較簡單,只需要比較Iface中的Itab的type是否與給定Type為同一個。
更多建議: