?# 11.1 接口是什么
Go 語(yǔ)言不是一種 “傳統(tǒng)” 的面向?qū)ο缶幊陶Z(yǔ)言:它里面沒(méi)有類和繼承的概念。
但是 Go 語(yǔ)言里有非常靈活的 接口 概念,通過(guò)它可以實(shí)現(xiàn)很多面向?qū)ο蟮奶匦?。接口提供了一種方式來(lái) 說(shuō)明 對(duì)象的行為:如果誰(shuí)能搞定這件事,它就可以用在這兒。
接口定義了一組方法(方法集),但是這些方法不包含(實(shí)現(xiàn))代碼:它們沒(méi)有被實(shí)現(xiàn)(它們是抽象的)。接口里也不能包含變量。
通過(guò)如下格式定義接口:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
上面的 Namer
是一個(gè) 接口類型。
(按照約定,只包含一個(gè)方法的)接口的名字由方法名加 [e]r
后綴組成,例如 Printer
、Reader
、Writer
、Logger
、Converter
等等。還有一些不常用的方式(當(dāng)后綴 er
不合適時(shí)),比如 Recoverable
,此時(shí)接口名以 able
結(jié)尾,或者以 I
開(kāi)頭(像 .NET
或 Java
中那樣)。
Go 語(yǔ)言中的接口都很簡(jiǎn)短,通常它們會(huì)包含 0 個(gè)、最多 3 個(gè)方法。
不像大多數(shù)面向?qū)ο缶幊陶Z(yǔ)言,在 Go 語(yǔ)言中接口可以有值,一個(gè)接口類型的變量或一個(gè) 接口值 :var ai Namer
,ai
是一個(gè)多字(multiword)數(shù)據(jù)結(jié)構(gòu),它的值是 nil
。它本質(zhì)上是一個(gè)指針,雖然不完全是一回事。指向接口值的指針是非法的,它們不僅一點(diǎn)用也沒(méi)有,還會(huì)導(dǎo)致代碼錯(cuò)誤。
此處的方法指針表是通過(guò)運(yùn)行時(shí)反射能力構(gòu)建的。
類型(比如結(jié)構(gòu)體)實(shí)現(xiàn)接口方法集中的方法,每一個(gè)方法的實(shí)現(xiàn)說(shuō)明了此方法是如何作用于該類型的:即實(shí)現(xiàn)接口,同時(shí)方法集也構(gòu)成了該類型的接口。實(shí)現(xiàn)了 Namer
接口類型的變量可以賦值給 ai
(接收者值),此時(shí)方法表中的指針會(huì)指向被實(shí)現(xiàn)的接口方法。當(dāng)然如果另一個(gè)類型(也實(shí)現(xiàn)了該接口)的變量被賦值給 ai
,這二者(譯者注:指針和方法實(shí)現(xiàn))也會(huì)隨之改變。
類型不需要顯式聲明它實(shí)現(xiàn)了某個(gè)接口:接口被隱式地實(shí)現(xiàn)。多個(gè)類型可以實(shí)現(xiàn)同一個(gè)接口。
實(shí)現(xiàn)某個(gè)接口的類型(除了實(shí)現(xiàn)接口方法外)可以有其他的方法。
一個(gè)類型可以實(shí)現(xiàn)多個(gè)接口。
接口類型可以包含一個(gè)實(shí)例的引用, 該實(shí)例的類型實(shí)現(xiàn)了此接口(接口是動(dòng)態(tài)類型)。
即使接口在類型之后才定義,二者處于不同的包中,被單獨(dú)編譯:只要類型實(shí)現(xiàn)了接口中的方法,它就實(shí)現(xiàn)了此接口。
所有這些特性使得接口具有很大的靈活性。
第一個(gè)例子:
示例 11.1 interfaces.go:
package main
import "fmt"
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func main() {
sq1 := new(Square)
sq1.side = 5
var areaIntf Shaper
areaIntf = sq1
// shorter,without separate declaration:
// areaIntf := Shaper(sq1)
// or even:
// areaIntf := sq1
fmt.Printf("The square has area: %f\n", areaIntf.Area())
}
輸出:
The square has area: 25.000000
上面的程序定義了一個(gè)結(jié)構(gòu)體 Square
和一個(gè)接口 Shaper
,接口有一個(gè)方法 Area()
。
在 main()
方法中創(chuàng)建了一個(gè) Square
的實(shí)例。在主程序外邊定義了一個(gè)接收者類型是 Square
方法的 Area()
,用來(lái)計(jì)算正方形的面積:結(jié)構(gòu)體 Square
實(shí)現(xiàn)了接口 Shaper
。
所以可以將一個(gè) Square
類型的變量賦值給一個(gè)接口類型的變量:areaIntf = sq1
。
現(xiàn)在接口變量包含一個(gè)指向 Square
變量的引用,通過(guò)它可以調(diào)用 Square
上的方法 Area()
。當(dāng)然也可以直接在 Square
的實(shí)例上調(diào)用此方法,但是在接口實(shí)例上調(diào)用此方法更令人興奮,它使此方法更具有一般性。接口變量里包含了接收者實(shí)例的值和指向?qū)?yīng)方法表的指針。
這是 多態(tài) 的 Go 版本,多態(tài)是面向?qū)ο缶幊讨幸粋€(gè)廣為人知的概念:根據(jù)當(dāng)前的類型選擇正確的方法,或者說(shuō):同一種類型在不同的實(shí)例上似乎表現(xiàn)出不同的行為。
如果 Square
沒(méi)有實(shí)現(xiàn) Area()
方法,編譯器將會(huì)給出清晰的錯(cuò)誤信息:
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)
如果 Shaper
有另外一個(gè)方法 Perimeter()
,但是Square
沒(méi)有實(shí)現(xiàn)它,即使沒(méi)有人在 Square
實(shí)例上調(diào)用這個(gè)方法,編譯器也會(huì)給出上面同樣的錯(cuò)誤。
擴(kuò)展一下上面的例子,類型 Rectangle
也實(shí)現(xiàn)了 Shaper
接口。接著創(chuàng)建一個(gè) Shaper
類型的數(shù)組,迭代它的每一個(gè)元素并在上面調(diào)用 Area()
方法,以此來(lái)展示多態(tài)行為:
示例 11.2 interfaces_poly.go:
package main
import "fmt"
type Shaper interface {
Area() float32
}
type Square struct {
side float32
}
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
type Rectangle struct {
length, width float32
}
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
r := Rectangle{5, 3} // Area() of Rectangle needs a value
q := &Square{5} // Area() of Square needs a pointer
// shapes := []Shaper{Shaper(r), Shaper(q)}
// or shorter
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())
}
}
輸出:
Looping through shapes for area ...
Shape details: {5 3}
Area of this shape is: 15
Shape details: &{5}
Area of this shape is: 25
在調(diào)用 shapes[n].Area()
這個(gè)時(shí),只知道 shapes[n]
是一個(gè) Shaper
對(duì)象,最后它搖身一變成為了一個(gè) Square
或 Rectangle
對(duì)象,并且表現(xiàn)出了相對(duì)應(yīng)的行為。
也許從現(xiàn)在開(kāi)始你將看到通過(guò)接口如何產(chǎn)生 更干凈、更簡(jiǎn)單 及 更具有擴(kuò)展性 的代碼。在 11.12.3 中將看到在開(kāi)發(fā)中為類型添加新的接口是多么的容易。
下面是一個(gè)更具體的例子:有兩個(gè)類型 stockPosition
和 car
,它們都有一個(gè) getValue()
方法,我們可以定義一個(gè)具有此方法的接口 valuable
。接著定義一個(gè)使用 valuable
類型作為參數(shù)的函數(shù) showValue()
,所有實(shí)現(xiàn)了 valuable
接口的類型都可以用這個(gè)函數(shù)。
示例 11.3 valuable.go:
package main
import "fmt"
type stockPosition struct {
ticker string
sharePrice float32
count float32
}
/* method to determine the value of a stock position */
func (s stockPosition) getValue() float32 {
return s.sharePrice * s.count
}
type car struct {
make string
model string
price float32
}
/* method to determine the value of a car */
func (c car) getValue() float32 {
return c.price
}
/* contract that defines different things that have value */
type valuable interface {
getValue() float32
}
func showValue(asset valuable) {
fmt.Printf("Value of the asset is %f\n", asset.getValue())
}
func main() {
var o valuable = stockPosition{"GOOG", 577.20, 4}
showValue(o)
o = car{"BMW", "M3", 66500}
showValue(o)
}
輸出:
Value of the asset is 2308.800049
Value of the asset is 66500.000000
一個(gè)標(biāo)準(zhǔn)庫(kù)的例子
io
包里有一個(gè)接口類型 Reader
:
type Reader interface {
Read(p []byte) (n int, err error)
}
定義變量 r
:var r io.Reader
那么就可以寫如下的代碼:
var r io.Reader
r = os.Stdin // see 12.1
r = bufio.NewReader(r)
r = new(bytes.Buffer)
f,_ := os.Open("test.txt")
r = bufio.NewReader(f)
上面 r
右邊的類型都實(shí)現(xiàn)了 Read()
方法,并且有相同的方法簽名,r
的靜態(tài)類型是 io.Reader
。
備注
有的時(shí)候,也會(huì)以一種稍微不同的方式來(lái)使用接口這個(gè)詞:從某個(gè)類型的角度來(lái)看,它的接口指的是:它的所有導(dǎo)出方法,只不過(guò)沒(méi)有顯式地為這些導(dǎo)出方法額外定一個(gè)接口而已。
練習(xí) 11.1 simple_interface.go:
定義一個(gè)接口 Simpler
,它有一個(gè) Get()
方法和一個(gè) Set()
,Get()
返回一個(gè)整型值,Set()
有一個(gè)整型參數(shù)。創(chuàng)建一個(gè)結(jié)構(gòu)體類型 Simple
實(shí)現(xiàn)這個(gè)接口。
接著定一個(gè)函數(shù),它有一個(gè) Simpler
類型的參數(shù),調(diào)用參數(shù)的 Get()
和 Set()
方法。在 main
函數(shù)里調(diào)用這個(gè)函數(shù),看看它是否可以正確運(yùn)行。
練習(xí) 11.2 interfaces_poly2.go:
a) 擴(kuò)展 interfaces_poly.go 中的例子,添加一個(gè) Circle
類型
b) 使用一個(gè)抽象類型 Shape
(沒(méi)有字段) 實(shí)現(xiàn)同樣的功能,它實(shí)現(xiàn)接口 Shaper
,然后在其他類型里內(nèi)嵌此類型。擴(kuò)展 10.6.5 中的例子來(lái)說(shuō)明覆寫。