在經(jīng)典的面向?qū)ο笳Z言(像 C++,Java 和 C#)中數(shù)據(jù)和方法被封裝為 類
的概念:類包含它們兩者,并且不能剝離。
Go 沒有類:數(shù)據(jù)(結(jié)構(gòu)體或更一般的類型)和方法是一種松耦合的正交關(guān)系。
Go 中的接口跟 Java/C# 類似:都是必須提供一個(gè)指定方法集的實(shí)現(xiàn)。但是更加靈活通用:任何提供了接口方法實(shí)現(xiàn)代碼的類型都隱式地實(shí)現(xiàn)了該接口,而不用顯式地聲明。
和其它語言相比,Go 是唯一結(jié)合了接口值,靜態(tài)類型檢查(是否該類型實(shí)現(xiàn)了某個(gè)接口),運(yùn)行時(shí)動(dòng)態(tài)轉(zhuǎn)換的語言,并且不需要顯式地聲明類型是否滿足某個(gè)接口。該特性允許我們?cè)诓桓淖円延械拇a的情況下定義和使用新接口。
接收一個(gè)(或多個(gè))接口類型作為參數(shù)的函數(shù),其實(shí)參可以是任何實(shí)現(xiàn)了該接口的類型。 實(shí)現(xiàn)了某個(gè)接口的類型可以被傳給任何以此接口為參數(shù)的函數(shù)
。
類似于 Python 和 Ruby 這類動(dòng)態(tài)語言中的 動(dòng)態(tài)類型(duck typing)
;這意味著對(duì)象可以根據(jù)提供的方法被處理(例如,作為參數(shù)傳遞給函數(shù)),而忽略它們的實(shí)際類型:它們能做什么比它們是什么更重要。
這在程序 duck_dance.go 中得以闡明,函數(shù) DuckDance 接受一個(gè) IDuck 接口類型變量。僅當(dāng) DuckDance 被實(shí)現(xiàn)了 IDuck 接口的類型調(diào)用時(shí)程序才能編譯通過。
示例 11.16 duck_dance.go:
package main
import "fmt"
type IDuck interface {
Quack()
Walk()
}
func DuckDance(duck IDuck) {
for i := 1; i <= 3; i++ {
duck.Quack()
duck.Walk()
}
}
type Bird struct {
// ...
}
func (b *Bird) Quack() {
fmt.Println("I am quacking!")
}
func (b *Bird) Walk() {
fmt.Println("I am walking!")
}
func main() {
b := new(Bird)
DuckDance(b)
}
輸出:
I am quacking!
I am walking!
I am quacking!
I am walking!
I am quacking!
I am walking!
如果 Bird
沒有實(shí)現(xiàn) Walk()
(把它注釋掉),會(huì)得到一個(gè)編譯錯(cuò)誤:
cannot use b (type *Bird) as type IDuck in function argument:
*Bird does not implement IDuck (missing Walk method)
如果對(duì) cat
調(diào)用函數(shù) DuckDance()
,Go 會(huì)提示編譯錯(cuò)誤,但是 Python 和 Ruby 會(huì)以運(yùn)行時(shí)錯(cuò)誤結(jié)束。
像 Python,Ruby 這類語言,動(dòng)態(tài)類型是延遲綁定的(在運(yùn)行時(shí)進(jìn)行):方法只是用參數(shù)和變量簡單地調(diào)用,然后在運(yùn)行時(shí)才解析(它們很可能有像 responds_to
這樣的方法來檢查對(duì)象是否可以響應(yīng)某個(gè)方法,但是這也意味著更大的編碼量和更多的測試工作)
Go 的實(shí)現(xiàn)與此相反,通常需要編譯器靜態(tài)檢查的支持:當(dāng)變量被賦值給一個(gè)接口類型的變量時(shí),編譯器會(huì)檢查其是否實(shí)現(xiàn)了該接口的所有函數(shù)。如果方法調(diào)用作用于像 interface{}
這樣的“泛型”上,你可以通過類型斷言(參見 11.3 節(jié))來檢查變量是否實(shí)現(xiàn)了相應(yīng)接口。
例如,你用不同的類型表示 XML 輸出流中的不同實(shí)體。然后我們?yōu)?XML 定義一個(gè)如下的“寫”接口(甚至可以把它定義為私有接口):
type xmlWriter interface {
WriteXML(w io.Writer) error
}
現(xiàn)在我們可以實(shí)現(xiàn)適用于該流類型的任何變量的 StreamXML
函數(shù),并用類型斷言檢查傳入的變量是否實(shí)現(xiàn)了該接口;如果沒有,我們就調(diào)用內(nèi)建的 encodeToXML
來完成相應(yīng)工作:
// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
if xw, ok := v.(xmlWriter); ok {
// It’s an xmlWriter, use method of asserted type.
return xw.WriteXML(w)
}
// No implementation, so we have to use our own function (with perhaps reflection):
return encodeToXML(v, w)
}
// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
// ...
}
Go 在這里用了和 gob
相同的機(jī)制:定義了兩個(gè)接口 GobEncoder
和 GobDecoder
。這樣就允許類型自己實(shí)現(xiàn)從流編解碼的具體方式;如果沒有實(shí)現(xiàn)就使用標(biāo)準(zhǔn)的反射方式。
因此 Go 提供了動(dòng)態(tài)語言的優(yōu)點(diǎn),卻沒有其他動(dòng)態(tài)語言在運(yùn)行時(shí)可能發(fā)生錯(cuò)誤的缺點(diǎn)。
對(duì)于動(dòng)態(tài)語言非常重要的單元測試來說,這樣即可以減少單元測試的部分需求,又可以發(fā)揮相當(dāng)大的作用。
Go 的接口提高了代碼的分離度,改善了代碼的復(fù)用性,使得代碼開發(fā)過程中的設(shè)計(jì)模式更容易實(shí)現(xiàn)。用 Go 接口還能實(shí)現(xiàn) 依賴注入模式
。
提取接口
是非常有用的設(shè)計(jì)模式,可以減少需要的類型和方法數(shù)量,而且不需要像傳統(tǒng)的基于類的面向?qū)ο笳Z言那樣維護(hù)整個(gè)的類層次結(jié)構(gòu)。
Go 接口可以讓開發(fā)者找出自己寫的程序中的類型。假設(shè)有一些擁有共同行為的對(duì)象,并且開發(fā)者想要抽象出這些行為,這時(shí)就可以創(chuàng)建一個(gè)接口來使用。
我們來擴(kuò)展 11.1 節(jié)的示例 11.2 interfaces_poly.go,假設(shè)我們需要一個(gè)新的接口 TopologicalGenus
,用來給 shape 排序(這里簡單地實(shí)現(xiàn)為返回 int)。我們需要做的是給想要滿足接口的類型實(shí)現(xiàn) Rank()
方法:
示例 11.17 multi_interfaces_poly.go:
//multi_interfaces_poly.go
package main
import "fmt"
type Shaper interface {
Area() float32
}
type TopologicalGenus interface {
Rank() int
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func (sq *Square) Rank() int {
return 1
}
type Rectangle struct {
length, width float32
}
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func (r Rectangle) Rank() int {
return 2
}
func main() {
r := Rectangle{5, 3} // Area() of Rectangle needs a value
q := &Square{5} // Area() of Square needs a pointer
shapes := []Shaper{r, q}
fmt.Println("Looping through shapes for area ...")
for n, _ := range shapes {
fmt.Println("Shape details: ", shapes[n])
fmt.Println("Area of this shape is: ", shapes[n].Area())
}
topgen := []TopologicalGenus{r, q}
fmt.Println("Looping through topgen for rank ...")
for n, _ := range topgen {
fmt.Println("Shape details: ", topgen[n])
fmt.Println("Topological Genus of this shape is: ", topgen[n].Rank())
}
}
輸出:
Looping through shapes for area ...
Shape details: {5 3}
Area of this shape is: 15
Shape details: &{5}
Area of this shape is: 25
Looping through topgen for rank ...
Shape details: {5 3}
Topological Genus of this shape is: 2
Shape details: &{5}
Topological Genus of this shape is: 1
所以你不用提前設(shè)計(jì)出所有的接口;整個(gè)設(shè)計(jì)可以持續(xù)演進(jìn),而不用廢棄之前的決定
。類型要實(shí)現(xiàn)某個(gè)接口,它本身不用改變,你只需要在這個(gè)類型上實(shí)現(xiàn)新的方法。
如果你希望滿足某個(gè)接口的類型顯式地聲明它們實(shí)現(xiàn)了這個(gè)接口,你可以向接口的方法集中添加一個(gè)具有描述性名字的方法。例如:
type Fooer interface {
Foo()
ImplementsFooer()
}
類型 Bar 必須實(shí)現(xiàn) ImplementsFooer
方法來滿足 Fooer
接口,以清楚地記錄這個(gè)事實(shí)。
type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}
大部分代碼并不使用這樣的約束,因?yàn)樗拗屏私涌诘膶?shí)用性。
但是有些時(shí)候,這樣的約束在大量相似的接口中被用來解決歧義。
在 6.1 節(jié)中, 我們看到函數(shù)重載是不被允許的。在 Go 語言中函數(shù)重載可以用可變參數(shù) ...T
作為函數(shù)最后一個(gè)參數(shù)來實(shí)現(xiàn)(參見 6.3 節(jié))。如果我們把 T 換為空接口,那么可以知道任何類型的變量都是滿足 T (空接口)類型的,這樣就允許我們傳遞任何數(shù)量任何類型的參數(shù)給函數(shù),即重載的實(shí)際含義。
函數(shù) fmt.Printf
就是這樣做的:
fmt.Printf(format string, a ...interface{}) (n int, errno error)
這個(gè)函數(shù)通過枚舉 slice
類型的實(shí)參動(dòng)態(tài)確定所有參數(shù)的類型。并查看每個(gè)類型是否實(shí)現(xiàn)了 String()
方法,如果是就用于產(chǎn)生輸出信息。我們可以回到 11.10 節(jié)查看這些細(xì)節(jié)。
當(dāng)一個(gè)類型包含(內(nèi)嵌)另一個(gè)類型(實(shí)現(xiàn)了一個(gè)或多個(gè)接口)的指針時(shí),這個(gè)類型就可以使用(另一個(gè)類型)所有的接口方法。
例如:
type Task struct {
Command string
*log.Logger
}
這個(gè)類型的工廠方法像這樣:
func NewTask(command string, logger *log.Logger) *Task {
return &Task{command, logger}
}
當(dāng) log.Logger
實(shí)現(xiàn)了 Log()
方法后,Task 的實(shí)例 task 就可以調(diào)用該方法:
task.Log()
類型可以通過繼承多個(gè)接口來提供像 多重繼承
一樣的特性:
type ReaderWriter struct {
*io.Reader
*io.Writer
}
上面概述的原理被應(yīng)用于整個(gè) Go 包,多態(tài)用得越多,代碼就相對(duì)越少(參見 12.8 節(jié))。這被認(rèn)為是 Go 編程中的重要的最佳實(shí)踐。
有用的接口可以在開發(fā)的過程中被歸納出來。添加新接口非常容易,因?yàn)橐延械念愋筒挥米儎?dòng)(僅僅需要實(shí)現(xiàn)新接口的方法)。已有的函數(shù)可以擴(kuò)展為使用接口類型的約束性參數(shù):通常只有函數(shù)簽名需要改變。對(duì)比基于類的 OO 類型的語言在這種情況下則需要適應(yīng)整個(gè)類層次結(jié)構(gòu)的變化。
練習(xí) 11.11:map_function_interface.go:
在練習(xí) 7.13 中我們定義了一個(gè) map 函數(shù)來使用 int 切片 (map_function.go)。
通過空接口和類型斷言,現(xiàn)在我們可以寫一個(gè)可以應(yīng)用于許多類型的 泛型
的 map 函數(shù),為 int 和 string 構(gòu)建一個(gè)把 int 值加倍和連接字符串值的 map 函數(shù) mapFunc
。
提示:為了可讀性可以定義一個(gè) interface{} 的別名,比如:type obj interface{}
練習(xí) 11.12:map_function_interface_var.go:
稍微改變練習(xí) 11.9,允許 mapFunc
接收不定數(shù)量的 items。
練習(xí) 11.13:main_stack.go—stack/stack_general.go:
在練習(xí) 10.10 和 10.11 中我們開發(fā)了一些棧結(jié)構(gòu)類型。但是它們被限制為某種固定的內(nèi)建類型。現(xiàn)在用一個(gè)元素類型是 interface{}(空接口)的切片開發(fā)一個(gè)通用的棧類型。
實(shí)現(xiàn)下面的棧方法:
Len() int
IsEmpty() bool
Push(x interface{})
Pop() (x interface{}, error)
Pop()
改變棧并返回最頂部的元素;Top()
只返回最頂部元素。
在主程序中構(gòu)建一個(gè)充滿不同類型元素的棧,然后彈出并打印所有元素的值。