golang interface深度解析

 一 接口介紹

    如果說gorountine和channel是支撐起Go語言的併發模型的基石,讓Go語言在如今集羣化與多核化的時代成爲一道亮麗的風景,那麼接口是Go語言整個類型系列的基石,讓Go語言在基礎編程哲學的探索上達到前所未有的高度。Go語言在編程哲學上是變革派,而不是改良派。這不是因爲Go語言有gorountine和channel,而更重要的是因爲Go語言的類型系統,更是因爲Go語言的接口。Go語言的編程哲學因爲有接口而趨於完美。C++,Java 使用"侵入式"接口,主要表現在實現類需要明確聲明自己實現了某個接口。這種強制性的接口繼承方式是面向對象編程思想發展過程中一個遭受相當多質疑的特性。Go語言採用的是“非侵入式接口",Go語言的接口有其獨到之處:只要類型T的公開方法完全滿足接口I的要求,就可以把類型T的對象用在需要接口I的地方,所謂類型T的公開方法完全滿足接口I的要求,也即是類型T實現了接口I所規定的一組成員。這種做法的學名叫做Structural Typing,有人也把它看作是一種靜態的Duck Typing。

     在類型中有一個重要的類別就是接口類型,表達了固定的一個方法集合。一個接口變量可以存儲任意實際值(非接口),只要這個值實現了接口的方法。

type Reader interface {
    Read(p []byte) (n int, err os.Error)
}

// Writer 是包裹了基礎 Write 方法的接口。
type Writer interface {
    Write(p []byte) (n int, err os.Error)
}

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
     有一個事情是一定要明確的,不論 r 保存了什麼值,r 的類型總是 io.Reader,Go 是靜態類型,而 r 的靜態類型是 io.Reader。接口類型的一個極端重要的例子是空接口interface{},它表示空的方法集合,由於任何值都有零個或者多個方法,所以任何值都可以滿足它。也有人說 Go 的接口是動態類型的,不過這是一種誤解。 它們是靜態類型的:接口類型的變量總是有着相同的靜態類型,這個值總是滿足空接口,只是存儲在接口變量中的值運行時可能被改變。對於所有這些都必須嚴謹的對待,因爲反射和接口密切相關。

二  接口類型內存佈局

    在類型中有一個重要的類別就是接口類型,表達了固定的一個方法集合。一個接口變量可以存儲任意實際值(非接口),只要這個值實現了接口的方法。interface在內存上實際由兩個成員組成,如下圖,tab指向虛表,data則指向實際引用的數據。虛表描繪了實際的類型信息及該接口所需要的方法集。

type Stringer interface {
    String() string
}
 
type Binary uint64
 
func (i Binary) String() string {
    return strconv.FormatUint(i.Get(), 2)
}
 
func (i Binary) Get() uint64 {
    return uint64(i)
}
 
func main() {
    var b Binary = 32
    s := Stringer(b)
    fmt.Print(s.String())
} 

  

      觀察itable的結構,首先是描述type信息的一些元數據,然後是滿足Stringger接口的函數指針列表(注意,這裏不是實際類型Binary的函數指針集哦)。因此我們如果通過接口進行函數調用,實際的操作其實就是s.tab->fun[0](s.data)。是不是和C++的虛表很像?但是他們有本質的區別。先看C++,它爲每個類創建了一個方法集即虛表,當子類重寫父類的虛函數時,就將表中的相應函數指針改爲子類自己實現的函數,如果沒有則指向父類的實現,當面臨多繼承時,C++對象結構裏就會存在多個虛表指針,每個虛表指針指向該方法集的不同部分。我們再來看golang的實現方式,同C++一樣,golang也爲每種類型創建了一個方法集,不同的是接口的虛表是在運行時專門生成的,而c++的虛表是在編譯時生成的(但是c++虛函數表表現出的多態是在運行時決定的).例如,當例子中當首次遇見s := Stringer(b)這樣的語句時,golang會生成Stringer接口對應於Binary類型的虛表,並將其緩存。那麼爲什麼go不採用c++的方式來實現呢?這根c++和golang的對象內存佈局是有關係的。

   首先c++的動態多態是以繼承爲基礎的,在對象構造初始化的時首先會初始化父類,其次是子類,也就是說一個對象的內存佈局是虛表,父類部分,子類部分(編譯器不同可能會有差異),當一個父類指針指向子類時,會發生內存的截斷,截斷子類部分(內存地址偏移),但是此時子類的虛表中的函數指針實際上還是指向了自己的實現,所以此時的指針纔會調用到子類的虛函數,如果不是虛函數,因爲內存已經截斷沒有子類的非虛函數信息了,所以只能調用父類的了,這種繼承關係讓c++的虛表的初始化非常清晰,在一個對象初始化時先調用父類的構造此時虛表跟父類是一樣的,接下來初始化子類,此時編譯器就會去識別子類有沒有覆蓋父類的虛函數,如果有則虛表中相應的函數指針改成自己的虛函數實現指針。

  那麼go有什麼不同呢,首先我們很清楚go是沒有嚴格意義上的繼承的,go的接口不存在繼承關係,只要實現了接口定義的方法都可以成爲接口類型,這給go的虛表初始化帶來很大的麻煩,到底有多少類型實現了這個接口,一個類型到底實現了多少接口這讓編譯器很confused。舉個例子,某個類型有m個方法,某接口有n個方法,則很容易知道這種判定的時間複雜度爲O(mXn),不過可以使用預先排序的方式進行優化,實際的時間複雜度爲O(m+n)這樣看來其實還行那爲什麼要在運行時生成虛表呢,這不是會拖慢程序的運行速度嗎,注意我們這裏是某個類型,某個接口,是1對1的關係,如果有n個類型,n個接口呢,編譯器難道要把之間所有的關係都理清嗎?退一步說就算編譯器任勞任怨把這事幹了,可是你在寫過程中你本來就不想實現那個接口,而你無意中給這個類型實現的方法中包含了某些接口的方法,你根本不需要這個接口(況且go的接口機制會導致很多這種無意義的接口實現),你欺負編譯器就行了,這也太欺負人了吧。如果我們放到運行時呢,我們只要在需要接口的去分析一下類型是否實現了接口的所有方法就行了很簡單的一件事。

三 空接口

    接口類型的一個極端重要的例子是空接口:interface{},它表示空的方法集合,由於任何值都有零個或者多個方法,所以任何值都可以滿足它。 注意,[]T不能直接賦值給[]interface{}

//t := []int{1, 2, 3, 4}   wrong
//var s []interface{} = t 
t := []int{1, 2, 3, 4}   //right
s := make([]interface{}, len(t))
for i, v := range t {
    s[i] = v
}
str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

在Go語言中,我們可以使用type switch語句查詢接口變量的真實數據類型,語法如下:

type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str //type of str is string
case Stringer: //type of str is Stringer
    return str.String()
}
也可以使用“comma, ok”的習慣用法來安全地測試值是否爲一個字符串:

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}
四 接口賦值

  package main

  import (
  "fmt"
  )

  type LesssAdder interface {
      Less(b Integer) bool
      Add(b Integer)
  }

  type Integer int

  func (a Integer) Less(b Integer) bool {
      return a < b
  }

  func (a *Integer) Add(b Integer) {
      *a += b
  }

  func main() {

      var a Integer = 1
      var b LesssAdder = &a
      fmt.Println(b)

      //var c LesssAdder = a
      //Error:Integer does not implement LesssAdder 
      //(Add method has pointer receiver)
  }
    go語言可以根據下面的函數:
func (a Integer) Less(b Integer) bool 
自動生成一個新的Less()方法

func (a *Integer) Less(b Integer) bool 
這樣,類型*Integer就既存在Less()方法,也存在Add()方法,滿足LessAdder接口。 而根據
func (a *Integer) Add(b Integer)
這個函數無法生成以下成員方法:
func(a Integer) Add(b Integer) {
    (&a).Add(b)
}

因爲(&a).Add()改變的只是函數參數a,對外部實際要操作的對象並無影響(值傳遞),這不符合用戶的預期。所以Go語言不會自動爲其生成該函數。因此類型Integer只存在Less()方法,缺少Add()方法,不滿足LessAddr接口。(可以這樣去理解:指針類型的對象函數是可讀可寫的,非指針類型的對象函數是隻讀的)將一個接口賦值給另外一個接口 在Go語言中,只要兩個接口擁有相同的方法列表(次序不同不要緊),那麼它們就等同的,可以相互賦值。 如果A接口的方法列表時接口B的方法列表的子集,那麼接口B可以賦值給接口A,但是反過來是不行的,無法通過編譯。



    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章