前言
class
和interface
在高級語言中是很重要的概念究恤。class
是對模型的定義和封裝,interface
則是對行為的抽象和封裝杆故。Go語言雖然沒有class
,但是有struct
和interface
,以另一種方式實現(xiàn)同樣的效果。
本文將談一談Go語言這與別不同的interface
的基本概念和一些需要注意的地方。
聲明interface
type Birds interface {
Twitter() string
Fly(high int) bool
}
上面這段代碼聲明了一個名為Birds
的接口類型(interface),這個接口包含兩個行為Twitter
和Fly
赐纱。
Go語言里面脊奋,聲明一個接口類型需要使用type
關(guān)鍵字、接口類型名稱疙描、interface
關(guān)鍵字和一組有{}
括起來的方法聲明诚隙,這些方法聲明只有方法名、參數(shù)和返回值起胰,不需要方法體久又。
Go語言沒有繼承的概念,那如果需要實現(xiàn)繼承的效果怎么辦效五?Go的方法是嵌入
地消。
type Chicken interface {
Bird
Walk()
}
上面這段代碼中聲明了一個新的接口類型Chicken
,我們希望他能夠共用Birds
的行為畏妖,于是直接在Chicken
的接口類型聲明中脉执,嵌入Birds
接口類型,這樣Chicken
接口中就有了原屬于Birds
的Twitter
和Fly
這兩個行為以及新增加的Walk
行為戒劫,實現(xiàn)了接口繼承的效果半夷。
實現(xiàn)interface
在java中,通過類來實現(xiàn)接口迅细。一個類需要在聲明通過implements
顯示說明實現(xiàn)哪些接口巫橄,并在類的方法中實現(xiàn)所有的接口方法。Go語言沒有類茵典,也沒有implements
湘换,如何來實現(xiàn)一個接口呢?這里就體現(xiàn)了Go與別不同的地方了统阿。
首先彩倚,Go語言沒有類但是有struct,通過struct來定義模型結(jié)構(gòu)和方法扶平。
其次帆离,Go語言實現(xiàn)一個接口并不需要顯示聲明,而是只要你實現(xiàn)了接口中的所有方法就認(rèn)為你實現(xiàn)了這個接口蜻直。這稱之為Duck typing
盯质。
如果它走起步來像鴨子,并且叫聲像鴨子, 那個它一定是一只鴨子.
說道這里袁串,就需要介紹下struct如何實現(xiàn)方法概而。
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
關(guān)鍵字用于聲明方法和函數(shù)餐曼,后面方法Fly
以及參數(shù)和返回值压储。但是在func
關(guān)鍵字和方法名Fly
中間還有s *Sparraw
的聲明,這個聲明在Go中稱之為接收者聲明源譬,其中s
代表這個方法的接收者集惋,*Sparrow
代表這個接收者的類型。
接收者的類型可以為一個數(shù)據(jù)類型的指針類型踩娘,也可以是數(shù)據(jù)類型本身刮刑,比如我們針對Sparrow
再實現(xiàn)一個方法:
func (s Sparrow) Walk() {
// ...
}
接收者為數(shù)據(jù)類型的方法稱為值方法,接收者為指針類型的方法稱之為指針方法养渴。
這種非侵入式的接口實現(xiàn)方式非常的方便和靈活雷绢,不用去管理各種接口依賴,對開發(fā)人員來說也更簡潔理卑。
使用interface
利用struct去實現(xiàn)接口之后翘紊,我們就可以用這個struct作為接口參數(shù),使用那些接收接口參數(shù)的方法完成我們的功能藐唠。這也是面向接口編程的方式帆疟,我們的功能依據(jù)接口來實現(xiàn),而不用關(guān)心實現(xiàn)接口的是什么中捆,這樣大大提供了功能的通用性可擴(kuò)展性鸯匹。
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直接作為參數(shù)
BirdAnimation(sparrow, 1000)
}
上面這段代碼中,我們聲明了一個Birds
接口類型的變量bird
泄伪,由于*Sparrow
實現(xiàn)了Birds
接口的所有方法殴蓬,所以我們可以將*Sparrow
類型的變量sparrow
賦值給bird
◇危或者直接將sparrow
作為參數(shù)調(diào)用BirdAnimation
染厅,運(yùn)行結(jié)果如下:
? go run main.go
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow Fly
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow Fly
深入一步interface
關(guān)于空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
肖粮,但是我們并沒有對其進(jìn)行初始化,是一個nil
值尔苦,然后我們直接將它作為參數(shù)調(diào)用NilInterfaceTest()
涩馆,我們預(yù)期的結(jié)果是希望NilInterfaceTest
方法檢測出nil
值,避免出錯允坚。然而實際結(jié)果是這樣的:
? 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魂那,正常去使用最終導(dǎo)致了程序panic。
也許這里很讓人迷惑稠项,其實這里應(yīng)該認(rèn)識到雖然我們可以將實現(xiàn)了接口所有方法的接收者當(dāng)做接口來使用涯雅,但是兩者并不是完全等同。在Go語言中展运,interface的底層結(jié)構(gòu)其實是比較復(fù)雜的活逆,簡要來說精刷,一個interface結(jié)構(gòu)包含兩部分: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
變量的類型和值分別輸出,得到結(jié)果如下:
? 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部分就已經(jīng)有值了儿礼,只不過他的value部分是nil
,所以bird
并不是nil
庆寺。
關(guān)于方法列表
再看一段代碼:
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)
}
其運(yùn)行結(jié)果如下:
? 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
并沒有實現(xiàn)Chicken接口,因為Fly方法的接受者是指針接收者懦尝,而我們給的是Sparrow
知纷。
我們將程序做一點小小的調(diào)整就可以了,將第10行代碼修改為:
chicken = &sparrow2
也許你會問:"Chicken接口的Walk方法的接收者是非指針的Sparrow陵霉,我們把*Sparrow賦值給Chicken接口變量為什么可以通過琅轧?"。
這里就要講到方法列表的概念踊挠。
首先乍桂,一個指針類型的方法列表必然包含所有接收者為指針接收者的方法,同理非指針類型的方法列表也包含所有接收者為非指針類型的方法效床。在我們例子中*Sparrow
首先包含:Fly
和Twitter
睹酌;Sparrow
包含Walk
。
其次剩檀,當(dāng)我們擁有一個指針類型的時候憋沿,因為有了這個變量的地址,我們得到這個具體的變量沪猴,所以一個指針類型的方法列表還可以包含其非指針類型作為接收者的方法辐啄。在我們的例子中就是*Sparrow
的方法列表為:Fly
、Twitter
和Walk
运嗜,所以chicken = &sparrow2
可以通過壶辜。
但是一個非指針類型卻并不總是能取到它的地址,從而獲取它接收者為指針接收者的方法洗出。所以非指針類型的方法列表中只有接收者為非指針類型的方法士复。如果它的方法列表不能完全覆蓋這個接口图谷,是不算實現(xiàn)了這個接口的翩活。
舉個簡單的例子:
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()
}
這樣可以正常運(yùn)行,但是稍微改改:
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
字面量也無法取址菠镇。
因此在使用接口時冗荸,我們要注意不同類型的方法列表,是否實現(xiàn)接口利耍。