7.3. 實現接口的條件
一個類型如果擁有一個接口需要的所有方法,那麼這個類型就實現了這個接口。例如,*os.File
類型實現了io.Reader,Writer,Closer,和ReadWriter接口。*bytes.Buffer
實現了Reader,Writer,和ReadWriter這些接口,但是它沒有實現Closer接口因為它不具有Close方法。Go的程序員經常會簡要的把一個具體的類型描述成一個特定的接口類型。舉個例子,*bytes.Buffer
是io.Writer;*os.Files
是io.ReadWriter。
接口指定的規則非常簡單:表達一個類型屬於某個接口只要這個類型實現這個接口。所以:
var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method
這個規則甚至適用於等式右邊本身也是一個接口類型
w = rwc // OK: io.ReadWriteCloser has Write method
rwc = w // compile error: io.Writer lacks Close method
因為ReadWriter和ReadWriteCloser包含有Writer的方法,所以任何實現了ReadWriter和ReadWriteCloser的類型必定也實現了Writer接口
在進一步學習前,必須先解釋一個類型持有一個方法的表示當中的細節。回想在6.2章中,對於每一個命名過的具體類型T;它的一些方法的接收者是類型T本身然而另一些則是一個*T
的指針。還記得在T類型的參數上調用一個*T
的方法是合法的,只要這個參數是一個變量;編譯器隱式的獲取了它的地址。但這僅僅是一個語法糖:T類型的值不擁有所有*T
指針的方法,這樣它就可能只實現了更少的接口。
舉個例子可能會更清晰一點。在第6.5章中,IntSet類型的String方法的接收者是一個指針類型,所以我們不能在一個不能尋址的IntSet值上調用這個方法:
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
但是我們可以在一個IntSet變量上調用這個方法:
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method
然而,由於只有*IntSet
類型有String方法,所以也只有*IntSet
類型實現了fmt.Stringer接口:
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method
12.8章包含了一個打印出任意值的所有方法的程序,然後可以使用godoc -analysis=type tool(§10.7.4)展示每個類型的方法和具體類型和接口之間的關係
就像信封封裝和隱藏起信件來一樣,接口類型封裝和隱藏具體類型和它的值。即使具體類型有其它的方法,也只有接口類型暴露出來的方法會被調用到:
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
os.Stdout.Close() // OK: *os.File has Close method
var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer has Write method
w.Close() // compile error: io.Writer lacks Close method
一個有更多方法的接口類型,比如io.ReadWriter,和少一些方法的接口類型例如io.Reader,進行對比;更多方法的接口類型會告訴我們更多關於它的值持有的信息,並且對實現它的類型要求更加嚴格。那麼關於interface{}類型,它沒有任何方法,請講出哪些具體的類型實現了它?
這看上去好像沒有用,但實際上interface{}被稱為空接口類型是不可或缺的。因為空接口類型對實現它的類型沒有要求,所以我們可以將任意一個值賦給空接口類型。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
儘管不是很明顯,從本書最早的例子中我們就已經在使用空接口類型。它允許像fmt.Println或者5.7章中的errorf函數接受任何類型的參數。
對於創建的一個interface{}值持有一個boolean,float,string,map,pointer,或者任意其它的類型;我們當然不能直接對它持有的值做操作,因為interface{}沒有任何方法。我們會在7.10章中學到一種用類型斷言來獲取interface{}中值的方法。
因為接口與實現只依賴於判斷兩個類型的方法,所以沒有必要定義一個具體類型和它實現的接口之間的關係。也就是說,有意地在文檔裡說明或者程序上斷言這種關係偶爾是有用的,但程序上不強制這麼做。下面的定義在編譯期斷言一個*bytes.Buffer
的值實現了io.Writer接口類型:
// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)
因為任意*bytes.Buffer
的值,甚至包括nil通過(*bytes.Buffer)(nil)
進行顯示的轉換都實現了這個接口,所以我們不必分配一個新的變量。並且因為我們絕不會引用變量w,我們可以使用空標識符來進行代替。總的看,這些變化可以讓我們得到一個更樸素的版本:
// *bytes.Buffer must satisfy io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)
非空的接口類型比如io.Writer經常被指針類型實現,尤其當一個或多個接口方法像Write方法那樣隱式的給接收者帶來變化的時候。一個結構體的指針是非常常見的承載方法的類型。
但是並不意味著只有指針類型滿足接口類型,甚至連一些有設置方法的接口類型也可能會被Go語言中其它的引用類型實現。我們已經看過slice類型的方法(geometry.Path,§6.1)和map類型的方法(url.Values,§6.2.1),後面還會看到函數類型的方法的例子(http.HandlerFunc,§7.7)。甚至基本的類型也可能會實現一些接口;就如我們在7.4章中看到的time.Duration類型實現了fmt.Stringer接口。
一個具體的類型可能實現了很多不相關的接口。考慮在一個組織出售數字文化產品比如音樂,電影和書籍的程序中可能定義了下列的具體類型:
Album
Book
Movie
Magazine
Podcast
TVEpisode
Track
我們可以把每個抽象的特點用接口來表示。一些特性對於所有的這些文化產品都是共通的,例如標題,創作日期和作者列表。
type Artifact interface {
Title() string
Creators() []string
Created() time.Time
}
其它的一些特性只對特定類型的文化產品才有。和文字排版特性相關的只有books和magazines,還有隻有movies和TV劇集和屏幕分辨率相關。
type Text interface {
Pages() int
Words() int
PageSize() int
}
type Audio interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // e.g., "MP3", "WAV"
}
type Video interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string // e.g., "MP4", "WMV"
Resolution() (x, y int)
}
這些接口不止是一種有用的方式來分組相關的具體類型和表示他們之間的共同特點。我們後面可能會發現其它的分組。舉例,如果我們發現我們需要以同樣的方式處理Audio和Video,我們可以定義一個Streamer接口來代表它們之間相同的部分而不必對已經存在的類型做改變。
type Streamer interface {
Stream() (io.ReadCloser, error)
RunningTime() time.Duration
Format() string
}
每一個具體類型的組基於它們相同的行為可以表示成一個接口類型。不像基於類的語言,他們一個類實現的接口集合需要進行顯式的定義,在Go語言中我們可以在需要的時候定義一個新的抽象或者特定特點的組,而不需要修改具體類型的定義。當具體的類型來自不同的作者時這種方式會特別有用。當然也確實沒有必要在具體的類型中指出這些共性。