接口定義
在 Go 語言的語境中,當我們在談論“接口”的時候,一定指的是接口類型。因爲接口類型與其他數據類型不同,它是沒法被值化的,或者說是沒法被實例化的。
type 接口名 interface {
方法列表
}
接口類型聲明中的這些方法所代表的就是該接口的方法集合。一個接口的方法集合就是它的全部特徵
實現接口
對於任何數據類型,只要它的方法集合中完全包含了一個接口的全部特徵(即全部的方法),那麼它就一定是這個接口的實現類型。
怎樣判定一個數據類型的某一個方法實現的就是某個接口類型中的某個方法呢?
兩個充分必要條件,一個是“兩個方法的簽名需要完全一致”,另一個是“兩個方法的名稱要一模一樣”。
duck typing
是一種無侵入式的接口實現方式。
這是程序設計中的一種類型推斷風格,這種風格適用於動態語言(比如PHP、Python、Ruby、Typescript、Perl、Objective-C、Lua、Julia、JavaScript、Java、Groovy、C#等)和某些靜態語言(比如Golang,一般來說,靜態類型語言在編譯時便已確定了變量的類型,但是Golang的實現是:在編譯時推斷變量的類型),支持"鴨子類型"的語言的解釋器/編譯器將會在解析(Parse)或編譯時,推斷對象的類型。
" In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with" [2]
"鴨子類型"像多態一樣工作,但是沒有繼承。
“鴨子類型”的語言是這麼推斷的:一隻鳥走起來像鴨子、遊起泳來像鴨子、叫起來也像鴨子,那它就可以被當做鴨子。也就是說,它不關注對象的類型,而是關注對象具有的行爲(方法)。
動態類型&動態值&靜態類型
package main
import "fmt"
type Pet interface {
SetName(name string)
Name() string
Category() string
}
type Dog struct {
name string // 名字。
}
func (dog *Dog) SetName(name string) {
dog.name = name
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
func main() {
// 示例1。
dog := Dog{"little pig"}
_, ok := interface{}(dog).(Pet)
fmt.Printf("Dog implements interface Pet: %v\n", ok)
_, ok = interface{}(&dog).(Pet)
fmt.Printf("*Dog implements interface Pet: %v\n", ok)
fmt.Println()
// 示例2。
var pet Pet = &dog
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name())
}
對於一個接口類型的變量來說,我們賦給它的值可以被叫做它的實際值(也稱動態值),而該值的類型可以被叫做這個變量的實際類型(也稱動態類型)
對於變量pet來講,它的動態類型是*Dog,動態值就是&dog ,它的靜態類型就是Pet,並且永遠是Pet,但是它的動態類型卻會隨着我們賦給它的動態值而變化。
爲接口變量賦值
當我們爲一個接口變量賦值時會發生什麼?
首選記住一個規則:如果我們使用一個變量給另外一個變量賦值,那麼真正賦給後者的,並不是前者持有的那個值,而是該值的一個副本。
接口類型值的存儲方式和結構
接口類型本身是無法被值化的。在我們賦予它實際的值之前,它的值一定會是nil,這也是它的零值。
當我們給一個接口變量賦值的時候,該變量的動態類型會與它的動態值一起被存儲在一個專用的數據結構中。我們就把這個專用的數據結構叫做iface吧,在 Go 語言的runtime包中它其實就叫這個名字。iface的實例會包含兩個指針,一個是指向類型信息的指針,另一個是指向動態值的指針。這裏 的類型信息是由另一個專用數據結構的實例承載的,其中包含了動態值的類型,以及使它實現了接口的方法和調用它們的途徑,等等。總之,接口變量被賦予動態值的時候,存儲的是包含了這個動態值的副本的一個結構更加複雜的值。
接口變量賦值爲真正的nil
在 Go 語言中,我們把由字面量nil表示的值叫做無類型的nil。這是真正的nil,因爲它的類型也是nil的。
只要我們把一個有類型的nil賦給接口變量,那麼這個變量的值就一定不會是那個真正的nil。
怎樣才能讓一個接口變量的值真正爲nil呢?要麼只聲明它但不做初始化,要麼直接把字面量nil賦給它。
接口的組合
接口類型間的嵌入也被稱爲接口的組合。接口類型間的嵌入比結構體字段的嵌入要更簡單一些,因爲它不會涉及方法間的“屏蔽”。只要組合的接口之間有同名的方法就會產生衝突,從而無法通過編譯,即使同名方法的簽名彼此不同也會是如此。因此,接口的組合根本不可能導致“屏蔽”現象的出現。
Go 語言團隊鼓勵我們聲明體量較小的接口,並建議我們通過這種接口間的組合來擴展程序、增加程序的靈活性
這是因爲相比於包含很多方法的大接口而言,小接口可以更加專注地表達某一種能力或某一類特徵,同時也更容易被組合在一起。
Go 語言標準庫代碼包io中的ReadWriteCloser接口和ReadWriter接口就是這樣的例子,它們都是由若干個小接口組合而成的。以io.ReadWriteCloser接口爲例,它是由io.Reader、io.Writer和io.Closer這三個接口組成的。
這三個接口都只包含了一個方法,是典型的小接口。它們中的每一個都只代表了一種能力,分別是讀出、寫入和關閉。我們編寫這幾個小接口的實現類型通常都會很容易。並且,一旦我們同時實現了它們,就等於實現了它們的組合接口io.ReadWriteCloser。即使我們只實現了io.Reader和io.Writer,那麼也等同於實現了io.ReadWriter接口,因爲後者就是前兩個接口組成的。可以看到,這幾個io包中的接口共同組成了一個接口矩陣。它們既相互關聯又獨立存在