11.1 接口是什么
Go 語言不是一種“傳統(tǒng)”的面向對象編程語言:它里面沒有類和繼承的概念哆料。
但是 Go 語言里有非常靈活的接口概念,通過它可以實現(xiàn)很多面向對象的特性吗铐。接口提供了一種方式來說明對象的行為:如果誰能搞定這件事东亦,它就可以用在這兒。
接口定義了一組方法(方法集),但是這些方法不包含(實現(xiàn))代碼:它們沒有被實現(xiàn)(它們是抽象的)典阵。接口里也不能包含變量奋渔。
通過如下格式定義接口:
typeNamerinterface{Method1(param_list) return_typeMethod2(param_list) return_type? ? ...}
上面的Namer是一個接口類型。
(按照約定壮啊,只包含一個方法的)接口的名字由方法名加[e]r后綴組成嫉鲸,例如Printer、Reader歹啼、Writer玄渗、Logger、Converter等等狸眼。還有一些不常用的方式(當后綴er不合適時)藤树,比如Recoverable,此時接口名以able結尾拓萌,或者以I開頭(像.NET或Java中那樣)岁钓。
Go 語言中的接口都很簡短,通常它們會包含 0 個微王、最多 3 個方法屡限。
不像大多數(shù)面向對象編程語言,在 Go 語言中接口可以有值炕倘,一個接口類型的變量或一個接口值:var ai Namer钧大,ai是一個多字(multiword)數(shù)據(jù)結構,它的值是nil罩旋。它本質上是一個指針拓型,雖然不完全是一回事。指向接口值的指針是非法的瘸恼,它們不僅一點用也沒有,還會導致代碼錯誤册养。
此處的方法指針表是通過運行時反射能力構建的东帅。
類型(比如結構體)實現(xiàn)接口方法集中的方法,每一個方法的實現(xiàn)說明了此方法是如何作用于該類型的:即實現(xiàn)接口球拦,同時方法集也構成了該類型的接口靠闭。實現(xiàn)了Namer接口類型的變量可以賦值給ai(接收者值),此時方法表中的指針會指向被實現(xiàn)的接口方法坎炼。當然如果另一個類型(也實現(xiàn)了該接口)的變量被賦值給ai愧膀,這二者(譯者注:指針和方法實現(xiàn))也會隨之改變。
類型不需要顯式聲明它實現(xiàn)了某個接口:接口被隱式地實現(xiàn)谣光。多個類型可以實現(xiàn)同一個接口檩淋。
實現(xiàn)某個接口的類型(除了實現(xiàn)接口方法外)可以有其他的方法。
一個類型可以實現(xiàn)多個接口萄金。
接口類型可以包含一個實例的引用蟀悦, 該實例的類型實現(xiàn)了此接口(接口是動態(tài)類型)媚朦。
即使接口在類型之后才定義,二者處于不同的包中日戈,被單獨編譯:只要類型實現(xiàn)了接口中的方法询张,它就實現(xiàn)了此接口。
所有這些特性使得接口具有很大的靈活性浙炼。
第一個例子:
示例 11.1interfaces.go:
packagemainimport"fmt"typeShaperinterface{Area()float32}typeSquarestruct{? ? sidefloat32}func(sq*Square)Area()float32{returnsq.side* sq.side}funcmain() {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
上面的程序定義了一個結構體Square和一個接口Shaper份氧,接口有一個方法Area()。
在main()方法中創(chuàng)建了一個Square的實例弯屈。在主程序外邊定義了一個接收者類型是Square方法的Area()蜗帜,用來計算正方形的面積:結構體Square實現(xiàn)了接口Shaper。
所以可以將一個Square類型的變量賦值給一個接口類型的變量:areaIntf = sq1季俩。
現(xiàn)在接口變量包含一個指向Square變量的引用钮糖,通過它可以調用Square上的方法Area()。當然也可以直接在Square的實例上調用此方法酌住,但是在接口實例上調用此方法更令人興奮店归,它使此方法更具有一般性。接口變量里包含了接收者實例的值和指向對應方法表的指針酪我。
這是多態(tài)的 Go 版本消痛,多態(tài)是面向對象編程中一個廣為人知的概念:根據(jù)當前的類型選擇正確的方法,或者說:同一種類型在不同的實例上似乎表現(xiàn)出不同的行為都哭。
如果Square沒有實現(xiàn)Area()方法秩伞,編譯器將會給出清晰的錯誤信息:
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)
如果Shaper有另外一個方法Perimeter(),但是Square沒有實現(xiàn)它欺矫,即使沒有人在Square實例上調用這個方法纱新,編譯器也會給出上面同樣的錯誤。
擴展一下上面的例子穆趴,類型Rectangle也實現(xiàn)了Shaper接口脸爱。接著創(chuàng)建一個Shaper類型的數(shù)組,迭代它的每一個元素并在上面調用Area()方法未妹,以此來展示多態(tài)行為:
示例 11.2interfaces_poly.go:
packagemainimport"fmt"typeShaperinterface{Area()float32}typeSquarestruct{? ? sidefloat32}func(sq*Square)Area()float32{returnsq.side* sq.side}typeRectanglestruct{? ? length, widthfloat32}func(rRectangle)Area()float32{returnr.length* r.width}funcmain() {r:=Rectangle{5,3}//Area() of Rectangle needs a valueq:=&Square{5}//Area() of Square needs a pointer//shapes := []Shaper{Shaper(r), Shaper(q)}//or shortershapes:=[]Shaper{r, q}? ? fmt.Println("Looping through shapes for area ...")forn,_:=rangeshapes {? ? ? ? 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
在調用shapes[n].Area())這個時簿废,只知道shapes[n]是一個Shaper對象,最后它搖身一變成為了一個Square或Rectangle對象络它,并且表現(xiàn)出了相對應的行為族檬。
也許從現(xiàn)在開始你將看到通過接口如何產(chǎn)生更干凈、更簡單及更具有擴展性的代碼化戳。在 11.12.3 中將看到在開發(fā)中為類型添加新的接口是多么的容易单料。
下面是一個更具體的例子:有兩個類型stockPosition和car,它們都有一個getValue()方法,我們可以定義一個具有此方法的接口valuable看尼。接著定義一個使用valuable類型作為參數(shù)的函數(shù)showValue()递鹉,所有實現(xiàn)了valuable接口的類型都可以用這個函數(shù)。
示例 11.3valuable.go:
packagemainimport"fmt"typestockPositionstruct{? ? tickerstringsharePricefloat32countfloat32}/*method to determine the value of a stock position*/func(sstockPosition)getValue()float32{returns.sharePrice* s.count}typecarstruct{makestringmodelstringpricefloat32}/*method to determine the value of a car*/func(ccar)getValue()float32{returnc.price}/*contract that defines different things that have value*/typevaluableinterface{getValue()float32}funcshowValue(assetvaluable) {? ? fmt.Printf("Value of the asset is%f\n", asset.getValue())}funcmain() {varovaluable = 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
一個標準庫的例子
io包里有一個接口類型Reader:
typeReaderinterface{Read(p []byte) (nint, errerror)}
定義變量r:var r io.Reader
那么就可以寫如下的代碼:
varrio.Readerr = os.Stdin//see 12.1r = bufio.NewReader(r)? ? r =new(bytes.Buffer)? ? f,_:=os.Open("test.txt")? ? r = bufio.NewReader(f)
上面r右邊的類型都實現(xiàn)了Read()方法藏斩,并且有相同的方法簽名躏结,r的靜態(tài)類型是io.Reader。
備注
有的時候狰域,也會以一種稍微不同的方式來使用接口這個詞:從某個類型的角度來看媳拴,它的接口指的是:它的所有導出方法,只不過沒有顯式地為這些導出方法額外定一個接口而已兆览。
練習 11.1simple_interface.go:
定義一個接口Simpler屈溉,它有一個Get()方法和一個Set(),Get()返回一個整型值抬探,Set()有一個整型參數(shù)子巾。創(chuàng)建一個結構體類型Simple實現(xiàn)這個接口。
接著定一個函數(shù)小压,它有一個Simpler類型的參數(shù)线梗,調用參數(shù)的Get()和Set()方法。在main函數(shù)里調用這個函數(shù)怠益,看看它是否可以正確運行仪搔。
練習 11.2interfaces_poly2.go:
a) 擴展 interfaces_poly.go 中的例子,添加一個Circle類型
b) 使用一個抽象類型Shape(沒有字段) 實現(xiàn)同樣的功能蜻牢,它實現(xiàn)接口Shaper烤咧,然后在其他類型里內嵌此類型。擴展 10.6.5 中的例子來說明覆寫抢呆。