Go語言的接口

概述

接口是一種抽象類型,是對其他類型行爲的概括與抽象,從語法角度來看,接口是一組方法定義的集合。很多面向對象的語言都有接口這個概念,但Go語言接口的獨特之處在於它是隱式實現。換句話說,對於一個具體的類型,無須聲明它實現了哪些接口,只要提供接口所必需的方法即可。這種設計讓編程人員無須改變已有類型的實現就可以爲這些類型創建新的接口——對於那些不能修改包的類型,這一點特別有用。

以Go標準庫中的fmt.Printf和fmt.Sprintf爲例。前者把結果傳送到標準輸出,後者把結果以string類型返回。輸出的實現中格式化是最複雜的部分,通過接口機制可以複用這部分實現。其實兩個函數都封裝了第三個函數fmt.Fprintf,這個函數對結果輸出到哪裏並不關心:

package fmt

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

func Printf(format string, args ...interface{}) (int, error) {
    return Fprintf(os.Stdout, format, args...)
}

func Sprintf(format string, args ...interface{}) string {
    var buf bytes.Buffer
    Fprintf(&buf, format args...)
    return buf.String()
}

Fprintf的第一個參數w是接口類型,其聲明如下:

package io

type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer接口定義了Fprintf和調用者之間的約定。一方面,這個約定要求調用者提供的具體類型(如Printf中的*os.File類型的參數os.Stdout或Sprintf中的*bytes.Buffer類型的參數&buf)包含一個與其簽名和行爲一致的Write方法。另一方面,這個約定保證了Fprintf能夠使用任何滿足io.Writer接口的參數。Fprintf只需要能調用參數的Write函數,無須假設它寫入的是一個文件還是一段內存。

我們也可以創建一個新類型來實現io.Writer接口:

type ByteCounter int

func (c *ByteCounter) Write(p []byte) (int, error) {
    *c += ByteCounter(len(p))
    return len(p), nil
}

因爲*Bytecounter滿足io.Writer接口的約定,所以可以在Fpirntf中使用它:

var c ByteCounter
fmt.Fprintf(&c, "Hello World")
fmt.Println(c) //輸出 11

嵌入式接口

一個接口類型定義了一系列方法,如果一個具體類型要實現該接口,那麼必須實現接口類型定義中的所有方法。另外,我們也可以通過組合已有接口來得到一個新接口,這樣的語法稱爲嵌入式接口,它讓我們可以直接使用一個接口而不用逐一寫出這個接口所包含的方法。

package io

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

type Closer interface {
    Close() error
}

type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

實現接口

如果一個類型實現了接口要求的所有方法,那麼則稱這個類型實現了這個接口。接口的賦值規則也很簡單,僅當一個表達式實現了一個接口時,這個表達式纔可以賦給該接口。

var w io.Writer

w = os.Stdout //正確,*os.File有Write方法
w = new(bytes.Buffer) //正確,*bytes.Buffer有Write方法
w = time.Second //編譯錯誤,time.Duration沒有Write方法

當右側表達式也是一個接口時,該規則也有效,此時右側表達式的接口必須實現了左側接口類型的所有方法才被允許賦值。

空接口

一個不包含任何方法的接口稱爲空接口。雖然我們無法從空接口中獲得任何信息,看起來空接口沒有任何用途,但實際上空接口是不可缺少的。正因爲空接口類型對其實現類型沒有任何要求,所以我們可以把任何值賦給空接口類型。

var any interface{}

any = true
any = 12.34
any = "hello"
any = map[string]int{"one":1}

當然,即使我們創建了一個指向布爾值、浮點數、字符串、map或者其他類型的空接口,也無法直接使用其中的值,畢竟這個接口不包含任何方法。我們需要一個方法從空接口中還原出實際值,用類型斷言可以做到。 

接口值

從概念上講,一個接口類型的值(接口值):一個具體類型和該類型的一個值。二者稱爲接口的動態類型和動態值。對於像Go這樣的靜態類型語言,類型僅僅是一個編譯時的概念,所以類型不是一個值。

在Go語言中,變量總是初始化爲一個特定的值,接口也不例外。以下代碼將說明這一點:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

上述代碼中,第一行聲明瞭一個接口類型的變量w,此時w的類型和值都爲nil——接口的零值就是把它的動態類型和值都設置爲nil。一個接口值是否爲nil取決於它的動態類型,所以現在這是一個nil接口值,可以與nil相比較。

第二行把一個具體類型隱式轉換爲一個接口類型,這行代碼與 w = io.Writer(os.Stdout) 等價。此時,w的動態類型轉換爲*os.File,而其值則會設置爲os.Stdout的副本,即一個指向代表進程的標準輸出的os.File類型的指針。執行這行語句後,調用該接口值的方法,會實際調用(*os.File)的方法,如:w.Write等價於(*os.File).Write。第三行語句也同理。

第四行語句會將w的類型和值都設置爲nil,把w恢復到它聲明時的狀態。

綜上可知,接口值可以互相比較,兩個接口值的類型和值都是可比較的,且類型和值都相等時才相等。

 

                                                                                                                                                本文部分內容摘自《Go程序設計語言》

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