前言
class
和interface
在進階語言中是很重要的概念。class
是對模型的定義和封裝,interface
則是對行為的抽象和封裝。Go語言雖然沒有class
,但是有struct
和interface
,以另一種方式實現同樣的效果。
本文將談一談Go語言這與別不同的interface
的基本概念和一些需要注意的地方。
聲明interface
type Birds interface { Twitter() string Fly(high int) bool}
上面這段代碼聲明了一個名為Birds
的介面類型(interface),這個介面包含兩個行為Twitter
和Fly
。
Go語言裡面,聲明一個介面類型需要使用type
關鍵字、介面類型名稱、interface
關鍵字和一組有{}
括起來的方法聲明,這些方法聲明只有方法名、參數和傳回值,不需要方法體。
Go語言沒有繼承的概念,那如果需要實現繼承的效果怎麼辦?Go的方法是嵌入
。
type Chicken interface { Bird Walk()}
上面這段代碼中聲明了一個新的介面類型Chicken
,我們希望他能夠共用Birds
的行為,於是直接在Chicken
的介面型別宣告中,嵌入Birds
介面類型,這樣Chicken
介面中就有了原屬於Birds
的Twitter
和Fly
這兩個行為以及新增加的Walk
行為,實現了介面繼承的效果。
實現interface
在java中,通過類來實現介面。一個類需要在聲明通過implements
顯示說明實現哪些介面,並在類的方法中實現所有的介面方法。Go語言沒有類,也沒有implements
,如何來實現一個介面呢?這裡就體現了Go與別不同的地方了。
首先,Go語言沒有類但是有struct,通過struct來定義模型結構和方法。
其次,Go語言實現一個介面並不需要顯示聲明,而是只要你實現了介面中的所有方法就認為你實現了這個介面。這稱之為Duck typing
。
如果它走起步來像鴨子,並且叫聲像鴨子, 那個它一定是一隻鴨子.
說道這裡,就需要介紹下struct如何?方法。
type Sparrow struct { name string}func (s *Sparrow) Fly(hign int) bool { // ... return true}func (s *Sparrow) Twitter() string { // ... return fmt.Sprintf("%s,jojojo", s.name)}
上面這段代碼,聲明了一個名為Sparrow
的struct
,下面聲明了兩個方法。不過這個方法的聲明行為可能略微有點奇怪。
比如func (s *Sparrow) Fly(hign int) bool
中,func
關鍵字用於聲明方法和函數,後面方法Fly
以及參數和傳回值。但是在func
關鍵字和方法名Fly
中間還有s *Sparraw
的聲明,這個聲明在Go中稱之為接受者聲明,其中s
代表這個方法的接收者,*Sparrow
代表這個接收者的類型。
接收者的類型可以為一個資料類型的指標類型,也可以是資料類型本身,比如我們針對Sparrow
再實現一個方法:
func (s Sparrow) Walk() { // ...}
接收者為資料類型的方法稱為值方法,接收者為指標類型的方法稱之為指標方法。
這種非侵入式的介面實現方式非常的方便和靈活,不用去管理各種介面依賴,對開發人員來說也更簡潔。
使用interface
利用struct去實現介面之後,我們就可以用這個struct作為介面參數,使用那些接收介面參數的方法完成我們的功能。這也是面向介面編程的方式,我們的功能依據介面來實現,而不用關心實現介面的是什麼,這樣大大提供了功能的通用性可擴充性。
func BirdAnimation(bird Birds, high int) { fmt.Printf("BirdAnimation of %T\n", bird) bird.Twitter() bird.Fly(high)}func main() { var bird Birds sparrow := &Sparrow{} bird = sparrow BirdAnimation(bird, 1000) // 或者將sparrow直接作為參數 BirdAnimation(sparrow, 1000)}
上面這段代碼中,我們聲明了一個Birds
介面類型的變數bird
,由於*Sparrow
實現了Birds
介面的所有方法,所以我們可以將*Sparrow
類型的變數sparrow
賦值給bird
。或者直接將sparrow
作為參數調用BirdAnimation
,運行結果如下:
➜ go run main.goBirdAnimation of *main.SparrowSparrow TwitterSparrow FlyBirdAnimation of *main.SparrowSparrow TwitterSparrow Fly
深入一步interface
關於空interface
先看一段代碼,猜猜會輸出什麼。
func NilInterfaceTest(chicken Chicken) { if chicken == nil { fmt.Println("Sorry,It’s Nil") } else { fmt.Println("Animation Start!") ChickenAnimation(chicken) }}func main() { var sparrow3 *Sparrow NilInterfaceTest(sparrow3)}
我們聲明了一個*Sparrow
的變數sparrow3
,但是我們並沒有對其進行初始化,是一個nil
值,然後我們直接將它作為參數調用NilInterfaceTest()
,我們預期的結果是希望NilInterfaceTest
方法檢測出nil
值,避免出錯。然而實際結果是這樣的:
➜ go run main.goAnimation Start!ChickenAnimation of *main.Sparrowpanic: value method main.Sparrow.Walk called using nil *Sparrow pointergoroutine 1 [running]:...
NilInterfaceTest
方法並沒有檢測到我們傳的是一個nil
的sparrow,正常去使用最終導致了程式panic。
也許這裡很讓人迷惑,其實這裡應該認識到雖然我們可以將實現了介面所有方法的接收者當做介面來使用,但是兩者並不是完全等同。在Go語言中,interface的底層結構其實是比較複雜的,簡要來說,一個interface結構包含兩部分:1.這個介面值的類型;2.指向這個介面值的指標。我們稍微在NilInterfaceTest
代碼中加點東西看看:
func NilInterfaceTest(chicken Chicken) { if chicken == nil { fmt.Println("Sorry,It’s Nil") } else { fmt.Println("Animation Start!") fmt.Printf("type:%v,value:%v\n", reflect.TypeOf(chicken), reflect.ValueOf(chicken)) ChickenAnimation(chicken) }}
我們增加了第6行的代碼,將bird
變數的類型和值分別輸出,得到結果如下:
➜ go run main.goAnimation Start!type:*main.Sparrow,value:<nil>ChickenAnimation of *main.Sparrowpanic: value method main.Sparrow.Walk called using nil *Sparrow pointer...
我們可以看到bird
的type為*main.Sparrow
,而value為nil
。也就是說,我們將一個nil的*Sparrow
賦值給bird
後,這個bird
的type部分就已經有值了,只不過他的value部分是nil
,所以bird
並不是nil
。
關於方法列表
再看一段代碼:
func ChickenAnimation(chicken Chicken) { fmt.Printf("ChickenAnimation of %T\n", chicken) chicken.Walk() chicken.Twitter()}func main() { var chicken Chicken sparrow2 := Sparrow{} chicken = sparrow2 ChickenAnimation(chicken)}
其運行結果如下:
➜ go run main.go# command-line-arguments./main.go:70:10: cannot use sparrow2 (type Sparrow) as type Chicken in assignment: Sparrow does not implement Chicken (Fly method has pointer receiver)
編譯器編譯報錯,它說Sparrow
並沒有實現Chicken介面,因為Fly方法的接受者是指標接收者,而我們給的是Sparrow
。
我們將程式做一點小小的調整就可以了,將第10行代碼修改為:
chicken = &sparrow2
也許你會問:"Chicken介面的Walk方法的接收者是非指標的Sparrow,我們把*Sparrow賦值給Chicken介面變數為什麼可以通過?"。
這裡就要講到方法列表的概念。
首先,一個指標類型的方法列表必然包含所有接收者為指標接收者的方法,同理非指標類型的方法列表也包含所有接收者為非指標類型的方法。在我們例子中*Sparrow
首先包含:Fly
和Twitter
;Sparrow
包含Walk
。
其次,當我們擁有一個指標類型的時候,因為有了這個變數的地址,我們得到這個具體的變數,所以一個指標類型的方法列表還可以包含其非指標類型作為接收者的方法。在我們的例子中就是*Sparrow
的方法列表為:Fly
、Twitter
和Walk
,所以chicken = &sparrow2
可以通過。
但是一個非指標類型卻並不總是能取到它的地址,從而擷取它接收者為指標接收者的方法。所以非指標類型的方法列表中只有接收者為非指標類型的方法。如果它的方法列表不能完全覆蓋這個介面,是不算實現了這個介面的。
舉個簡單的例子:
type TestInt intfunc main() { &TestInt(7)}
編譯報錯,無法取址:
➜ go run main.go# command-line-arguments./main.go:77:2: cannot take the address of TestInt(7)./main.go:77:2: &TestInt(7) evaluated but not used
又或者:
func main() { sparrow4 := Sparrow{} sparrow4.Twitter()}
這樣可以正常運行,但是稍微改改:
func main() { Sparrow{}.Twitter()}
則編譯報錯:
➜ go run main.go# command-line-arguments./main.go:80:11: cannot call pointer method on Sparrow literal./main.go:80:11: cannot take the address of Sparrow literal
字面量也無法取址。
因此在使用介面時,我們要注意不同類型的方法列表,是否實現介面。