Go語言 interface介紹

2018-07-25 15:58 更新

interface是Go語言中最成功的設(shè)計(jì)之一,空的interface可以被當(dāng)作“鴨子”類型使用,它使得Go這樣的靜態(tài)語言擁有了一定的動態(tài)性,但卻又不損失靜態(tài)語言在類型安全方面擁有的編譯時(shí)檢查的優(yōu)勢。

依賴于接口而不是實(shí)現(xiàn),優(yōu)先使用組合而不是繼承,這是程序抽象的基本原則。但是長久以來以C++為代表的“面向?qū)ο蟆闭Z言曲解了這些原則,讓人們走入了誤區(qū)。為什么要將方法和數(shù)據(jù)綁死?為什么要有多重繼承這么變態(tài)的設(shè)計(jì)?面向?qū)ο笾凶顝?qiáng)調(diào)的應(yīng)該是對象間的消息傳遞,卻為什么被演繹成了封裝繼承和多態(tài)。面向?qū)ο笫欠駥?shí)現(xiàn)程序程序抽象的合理途徑,又或者是因?yàn)樗嬖谖覀兙驼J(rèn)為它合理了。歷史原因,中間出現(xiàn)了太多的錯(cuò)誤。不管怎么樣,Go的interface給我們打開了一扇新的窗。

那么,Go中的interface在底層是如何實(shí)現(xiàn)的呢?

Eface和Iface

interface實(shí)際上就是一個(gè)結(jié)構(gòu)體,包含兩個(gè)成員。其中一個(gè)成員是指向具體數(shù)據(jù)的指針,另一個(gè)成員中包含了類型信息??战涌诤蛶Х椒ǖ慕涌诼杂胁煌旅娣謩e是空接口和帶方法的接口是使用的數(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ù)域中包含了一個(gè)void*指針,和一個(gè)類型結(jié)構(gòu)體的指針。interface{}扮演的角色跟C語言中的void*是差不多的,Go中的任何對象都可以表示為interface{}。不同之處在于,interface{}中有類型信息,于是可以實(shí)現(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;
};

其實(shí)在前面我們已經(jīng)見過它了。精確的垃圾回收中,就是依賴Type結(jié)構(gòu)體中的gc域的。不同類型數(shù)據(jù)的類型信息結(jié)構(gòu)體并不完全一致,Type是類型信息結(jié)構(gòu)體中公共的部分,其中size描述類型的大小,hash數(shù)據(jù)的hash值,align是對齊,fieldAlgin是這個(gè)數(shù)據(jù)嵌入結(jié)構(gòu)體時(shí)的對齊,kind是一個(gè)枚舉值,每種類型對應(yīng)了一個(gè)編號。alg是一個(gè)函數(shù)指針的數(shù)組,存儲了hash/equal/print/copy四個(gè)函數(shù)操作。UncommonType是指向一個(gè)函數(shù)指針的數(shù)組,收集了這個(gè)類型的實(shí)現(xiàn)的所有方法。

在reflect包中有個(gè)KindOf函數(shù),返回一個(gè)interface{}的Type,其實(shí)該函數(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信息,而且還多了一個(gè)方法表fun[]。一個(gè)Iface中的具體類型中實(shí)現(xiàn)的方法會被拷貝到Itab的fun數(shù)組中。

具體類型向接口類型賦值

將具體類型數(shù)據(jù)賦值給interface{}這樣的抽象類型,中間會涉及到類型轉(zhuǎn)換操作。從接口類型轉(zhuǎn)換為具體類型(也就是反射),也涉及到了類型轉(zhuǎn)換。這個(gè)轉(zhuǎn)換過程中做了哪些操作呢?先看將具體類型轉(zhuǎn)換為接口類型。如果是轉(zhuǎn)換成空接口,這個(gè)過程比較簡單,就是返回一個(gè)Eface,將Eface中的data指針指向原型數(shù)據(jù),type指針會指向數(shù)據(jù)的Type結(jié)構(gòu)體。

將某個(gè)類型數(shù)據(jù)轉(zhuǎn)換為帶方法的接口時(shí),會復(fù)雜一些。中間涉及了一道檢測,該類型必須要實(shí)現(xiàn)了接口中聲明的所有方法才可以進(jìn)行轉(zhuǎn)換。這個(gè)檢測是在編譯過程中做的,我們可以做個(gè)測試:

type I interface {
    String()
}
var a int = 5
var b I = a

編譯會報(bào)錯(cuò):

cannot use a (type int) as type I in assignment:
    int does not implement I (missing String method)

說明具體類型轉(zhuǎn)換為帶方法的接口類型是在編譯過程中進(jìn)行檢測的。

那么這個(gè)檢測是如何實(shí)現(xiàn)的呢?在runtime下找到了iface.c文件,應(yīng)該是早期版本是在運(yùn)行時(shí)檢測留下的,其中有一個(gè)itab函數(shù)就是判斷某個(gè)類型是否實(shí)現(xiàn)了某個(gè)接口,如果是則返回一個(gè)Itab結(jié)構(gòu)體。

類型轉(zhuǎn)換時(shí)的檢測就是比較具體類型的方法表和接口類型的方法表,看具體類型是實(shí)現(xiàn)了接口類型所聲明的所有的方法。還記得Type結(jié)構(gòu)體中是有個(gè)UncommonType字段的,里面有張方法表,類型所實(shí)現(xiàn)的方法都在里面。而在Itab中有個(gè)InterfaceType字段,這個(gè)字段中也有一張方法表,就是這個(gè)接口所要求的方法。這兩處方法表都是排序過的,只需要一遍順序掃描進(jìn)行比較,應(yīng)該可以知道Type中否實(shí)現(xiàn)了接口中聲明的所有方法。最后還會將Type方法表中的函數(shù)指針,拷貝到Itab的fun字段中。

這里提到了三個(gè)方法表,有點(diǎn)容易把人搞暈,所以要解釋一下。

Type的UncommonType中有一個(gè)方法表,某個(gè)具體類型實(shí)現(xiàn)的所有方法都會被收集到這張表中。reflect包中的Method和MethodByName方法都是通過查詢這張表實(shí)現(xiàn)的。表中的每一項(xiàng)是一個(gè)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中也有一張方法表,這張方法表中是接口所聲明的方法。其中每一項(xiàng)是一個(gè)IMethod,數(shù)據(jù)結(jié)構(gòu)如下:

struct IMethod
{
    String *name;
    String *pkgPath;
    Type *type;
};

跟上面的Method結(jié)構(gòu)體對比可以發(fā)現(xiàn),這里是只有聲明沒有實(shí)現(xiàn)的。

Iface中的Itab的func域也是一張方法表,這張表中的每一項(xiàng)就是一個(gè)函數(shù)指針,也就是只有實(shí)現(xiàn)沒有聲明。

類型轉(zhuǎn)換時(shí)的檢測就是看Type中的方法表是否包含了InterfaceType的方法表中的所有方法,并把Type方法表中的實(shí)現(xiàn)部分拷到Itab的func那張表中。

reflect

reflect就是給定一個(gè)接口類型的數(shù)據(jù),得到它的具體類型的類型信息,它的Value等。reflect包中的TypeOf和ValueOf函數(shù)分別做這個(gè)事情。

還有像

    v, ok := i.(T)

這樣的語法,也是判斷一個(gè)接口i的具體類型是否為類型T,如果是則將其值返回給v。這跟上面的類型轉(zhuǎn)換一樣,也會檢測轉(zhuǎn)換是否合法。不過這里的檢測是在運(yùn)行時(shí)執(zhí)行的。在runtime下的iface.c文件中,有一系統(tǒng)的assetX2X函數(shù),比如runtime.assetE2T,runtime.assetI2T等等。這個(gè)實(shí)現(xiàn)起來比較簡單,只需要比較Iface中的Itab的type是否與給定Type為同一個(gè)。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號