[譯] Go數據結構-接口

原文 Go Data Structures: Interfaceshtml

做者 Russ Coxgolang

聲明:本文目的僅僅做爲我的mark,因此在翻譯的過程當中參雜了本身的思想甚至改變了部份內容。但因爲譯者水平有限,所寫文字或者代碼可能會誤導讀者,如發現文章有問題,請儘快告知,不勝感激。c#


一些知識點

  1. Method Set方法集合,Go中每一個類型都有其與之關聯的方法集合,interface類型的方法集合是其接口,除了interface類型的其餘類型T的方法集合是全部receiverT的全部方法,而類型*T的方法集合則是receiver*T或者T的全部方法
  2. 方法調用,若是x是類型T的實例,且表達式&x能夠生成一個指向類型*T的指針,那麼:假如*T的方法集合包含了someMethod方法而T沒有,x.someMethod()是有效的,其本質是(&x).someMethod()

正文

Go中的接口是容許咱們使用鴨子類型,但他和某些動態語言(好比Python)不一樣的是:Go編譯時會捕獲那些顯而易見的錯誤,好比當接口中定義了Read()方法時,若是咱們傳遞int類型,或者是即便咱們傳遞了一個有Read()方法的類型但參數的數量或者類型和接口中定義的不一致,都會致使報錯。來看一個簡單的接口例子:緩存

type ReadCloser interface {
    Read(b []byte) (n int, err os.Error)
    Close()
}

而後咱們就能夠定義一個接收ReadCloser類型的函數:數據結構

// 這個函數先調用Read()方法獲取請求的數據而後調用Close()方法
func ReadAndClose(r ReadCloser, buf []byte) (n int, err os.Error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    r.Close()
    return
}

任何一個實現了ReadCloser中所定義的方法(不只僅是方法名相同,方法的參數數量以及對應的類型也要相同,在原文中做者稱之爲signatures)的類型均可以傳遞到ReadAndClose函數中並執行,若是咱們在某個地方傳遞了一個int類型過去,Go在編譯的時候就會報錯,可是像Python則會是在運行時報錯。函數

同時,接口不只限於靜態檢查。咱們能夠動態檢查特定接口值是否具備其餘方法。好比:優化

type Stringer interface {
    String() string
}

func ToString(any interface{}) string {
    if v, ok := any.(Stringer); ok {
        return v.String()
    }
    switch v := any.(type) {
    case int:
        return strconv.Itoa(v)
    case float:
        return strconv.Ftoa(v, 'g', -1)
    }
    return "???"
}

any參數被定義爲空接口類型,也就是說在其中並無限定必須含有哪些方法,更進一步的說:任何類型均可以做爲參數傳遞進來。if語句中的comma ok賦值詢問是否能夠將any轉換爲具備String方法的Stringer類型的接口值。若是是的話,接下來的語句會執行String方法並返回一個字符串。不然,switch語句則會在結束前判斷其是否爲幾個基本類型而後執行響應的邏輯。ui

實現一個簡單的例子,有一個新的64位整數類型,他有一個以二進制形式打印值的String方法,還有一個Get方法:翻譯

type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

Binary類型的值能夠傳遞給ToString,即便程序從未說明Binary實現了Stringer接口,它也會使用String方法對其進行格式化。由於運行時能夠知道Binary有一個String方法,因此它實現了Stringer,即便Binary的做者從未據說過Stringer指針

這些示例代表,即便在編譯時檢查了全部隱式轉換,顯式地接口到接口的轉換也能夠在運行時查詢方法集。Effective Go中有更多的如何使用接口的例子。

接口值

含有"方法"概念的語言大部分都屬於兩個陣營中的一個(好比:C++ Java),要麼是靜態的爲全部方法調用預置一個表,要麼是調用時再查找(好比:Python)而後將其緩存起來。Go語言則是兩邊都沾一點:雖然他有方法集合表,但這個表是運行時構建。

先來作一個熱身,Binary是一個由兩個32位"字"組成的64位長的類型

Binary

接口類型的值表現爲兩個字(假設咱們處於32位系統中,那麼一個字就是32位,本文中如沒有特別聲明,默認爲32位系統),其中第一個字做爲指針指向真正的值的元數據(包含類型,方法列表),第二個字做爲指針指向真正的值。以下圖所示:s := Stringer(b)賦值語句會隱式的對這兩個字填充值。

接口的數據結構

接口的第一個字指向了我比較喜歡用的叫作interface table或者itable的東西。itable開頭是一個存儲了類型相關的元數據信息,接下來就是一個由函數指針組成的列表。注意:itable和接口類型相對應,而不是和動態類型。就咱們的例子而言:Stringer中的itable只是爲了Stringer而創建,只關聯了Stringer中定義的String方法,而像Binary中定義的Get方法則不在其範圍內。

接口的第二個字我稱之爲data,其存儲或者指向了實際的數據,這上面的例子中也就是指向了b。賦值語句var s Stringer = b實際上對b作了拷貝,而不是對b進行引用。存儲在接口中的值可能有任意大小,但接口只提供了一個字來專門存儲真實數據,因此賦值語句在堆上分配了一塊內存,並將該字設置爲對這塊內存的引用。

ps:++itable所指向的元數據是能夠被同一個類型的不一樣實例所共享的,而data則無法共享。++

若是咱們想要知道接口是否內含了一個特定的類型,就像上面代碼中的type swith 那樣,Go編譯器會產生相似於C語言中的s.tab->type表達式等效的代碼來得到類型指針而後檢查其是不是咱們所指望的類型。若是類型匹配,那麼值會經過s.data解引用copy過去。

若是咱們想要調用s.String(),Go編譯器會產生和C語言中s.tab->fun[0](s.data)表達式等效的代碼。他會從itable中找到並調用對應的函數指針,而後將data中存儲的數據做爲第一個參數傳遞過去(僅僅是在本例中)。若是運行 8g -S x.go(在文章末尾有詳解) 就能夠看到這個過程。須要注意的是:Go編譯器傳遞到itable中的是data中的值(32位)而不是該值所對應的Binary(64位)。一般負責執行接口調用的組件並不知道這個字表示啥,也不知道這個指針指向了多大的數據,相反的,接口代碼安排itable中的函數接收接口的datada這個32位長的指向原始數據的指針的形式來做爲參數傳遞。所以,本例中的函數指針是(*Binary).String而不是Binary.String

在本例中,咱們僅僅考慮了只有一個方法的接口的狀況,而包含多個方法的接口則是在itable的尾部擁有更長的函數指針列表。

計算itable

如今咱們已經知道itable長啥樣了,但咱們還不清楚他們是怎麼生成的。Go的動態類型轉換意味着:對於編譯器或者連接器來講,由於有太多的接口類型以及具體類型(能夠說是除接口類型之外的全部類型),預先計算出全部可能的itable是不合理的,並且若是這樣作的話極可能絕大多數咱們用不到。相反的,Go編譯器爲每個具體類型(Binary, int, func(map[int]string) 等等)生成一個用來描述類型的結構-類型描述結構。在元數據中,類型描述結構包含由該類型所實現的方法列表。類似的,編譯器也會爲每個接口類型(好比說: Stringer)生成一個不一樣類型的類型描述結構,這個結構裏面也包含了一個方法列表。接口運行時經過查找具體類型的方法表,再根據接口類型的方法表中所列出的每一個方法來計算itable。運行時會在計算出itable後將其緩存起來。因此這樣只需計算一次。

在咱們的簡單例子中,Stringer中的方法表中只有一個方法,而Binary的方法表中則有兩個方法,一般接口可能會有 ni 個方法,而具體的類型可能會有 nt 個方法,顯然爲了找到具體類型方法與接口方法的映射將會須要O(ni * nt)的時間,但咱們能夠作一些優化。經過對兩個方法表進行排序並進行同時處理,咱們能夠用O(ni + nt)的時間來完成這個映射的構建。

內存優化

咱們大致上有兩種方式來進行內存優化。

首先,若是是空接口interface {},由於空接口沒有定義任何方法,因此itable中的方法列表就是一個空的,也就是說其中就僅僅剩下了一個指向原始類型的指針。這種狀況下,咱們就能夠直接丟棄掉itable而後在第一個字中放一個指向原始類型的指針就能夠了。

空接口

編譯器根據一個接口是否含有方法,選用不一樣的接口結構。

而後,若是原始的值能夠直接放入字中,也就是說其小於32位,那麼咱們就不要在堆上申請空間來存儲了,直接把他放到data中就行了。

原始數據的長度小於等於字的長度

data中是存原始數據的指針仍是直接存原始數據取決於原始數據的大小(長度),編譯器管理每一個類型的方法列表中的函數,並根據data中是指針仍是原數據做出響應的處理。上面代碼中的Binary由於是64位的,因此data存儲的是指針,而itable的方法中存儲的是(*Binary).String;若是Binary是32位的,那麼data中存儲的就是原數據,itable中方法列表存儲的則是Binary.String

文末總結

  1. 編譯過程當中,編譯器會爲每個類型建立一個類型描述符,該類型描述符包含了該類型的方法集合。
  2. 除了接口類型的其餘類型(咱們能夠稱之爲具體類型)和程序中所定義的全部接口類型存在某些轉換關係,而當某個具體類型type A能夠轉換爲某個接口類型interface B時,咱們能夠認爲該具體類型A和該接口類型B存在轉換關係,轉換關係存儲在itable中,該itable對全部的A -> B轉換都通用。但由於咱們在程序中可能定義了許許多多的接口口類型於具體類型,因此咱們將"全部的轉換關係在編譯時預先生成出來"這種方式不可取,一是麻煩,二是會生成不少程序根本就用不到的itable,因此itable在運行時生成。
相關文章
相關標籤/搜索