前言
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.go
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow Fly
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow 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.go
Animation Start!
ChickenAnimation of *main.Sparrow
panic: value method main.Sparrow.Walk called using nil *Sparrow pointer
goroutine 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.go
Animation Start!
type:*main.Sparrow,value:<nil>
ChickenAnimation of *main.Sparrow
panic: 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 int
func 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
字面量也無法取址。
因此在使用接口時,我們要注意不同類型的方法列表,是否實現接口。