概述
在 Go 語言中,如果一個(gè)結(jié)構(gòu)體和一個(gè)嵌入字段同時(shí)實(shí)現(xiàn)了相同的接口會(huì)發(fā)生什么呢?我們猜一下,可能有兩個(gè)問題:
1.編譯器會(huì)因?yàn)槲覀兺瑫r(shí)有兩個(gè)接口實(shí)現(xiàn)而報(bào)錯(cuò)嗎?
2.如果編譯器接受這樣的定義,那么當(dāng)接口調(diào)用時(shí)編譯器要怎么確定該使用哪個(gè)實(shí)現(xiàn)?
在寫了一些測試代碼并認(rèn)真深入的讀了一下標(biāo)準(zhǔn)之后,我發(fā)現(xiàn)了一些有意思的東西,而且覺得很有必要分享出來,那么讓我們先從 Go 語言中的方法開始說起。
方法
Go 語言中同時(shí)有函數(shù)和方法。一個(gè)方法就是一個(gè)包含了接受者的函數(shù),接受者可以是命名類型或者結(jié)構(gòu)體類型的一個(gè)值或者是一個(gè)指針。所有給定類型的方法屬于該類型的方法集。
下面定義一個(gè)結(jié)構(gòu)體類型和該類型的一個(gè)方法:
復(fù)制代碼 代碼如下:
type User struct {
Name string
Email string
}
func (u User) Notify() error
首先我們定義了一個(gè)叫做 User 的結(jié)構(gòu)體類型,然后定義了一個(gè)該類型的方法叫做 Notify,該方法的接受者是一個(gè) User 類型的值。要調(diào)用 Notify 方法我們需要一個(gè) User 類型的值或者指針:
復(fù)制代碼 代碼如下:
// User 類型的值可以調(diào)用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()
// User 類型的指針同樣可以調(diào)用接受者是值的方法
alimon := User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()
在這個(gè)例子中當(dāng)我們使用指針時(shí),Go 調(diào)整和解引用指針使得調(diào)用可以被執(zhí)行。注意,當(dāng)接受者不是一個(gè)指針時(shí),該方法操作對(duì)應(yīng)接受者的值的副本(意思就是即使你使用了指針調(diào)用函數(shù),但是函數(shù)的接受者是值類型,所以函數(shù)內(nèi)部操作還是對(duì)副本的操作,而不是指針操作,參見:http://play.golang.org/p/DBhWU0p1Pv)。
我們可以修改 Notify 方法,讓它的接受者使用指針類型:
復(fù)制代碼 代碼如下:
func (u *User) Notify() error
再來一次之前的調(diào)用(注意:當(dāng)接受者是指針時(shí),即使用值類型調(diào)用那么函數(shù)內(nèi)部也是對(duì)指針的操作,參見:http://play.golang.org/p/SYBb4xPfPh):
復(fù)制代碼 代碼如下:
// User 類型的值可以調(diào)用接受者是指針的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()
// User 類型的指針同樣可以調(diào)用接受者是指針的方法
alimon := User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()
如果你不清楚到底什么時(shí)候該使用值,什么時(shí)候該使用指針作為接受者,你可以去看一下這篇介紹。這篇文章同時(shí)還包含了社區(qū)約定的接受者該如何命名。
接口
Go 語言中的接口很特別,而且提供了難以置信的一系列靈活性和抽象性。它們指定一個(gè)特定類型的值和指針表現(xiàn)為特定的方式。從語言角度看,接口是一種類型,它指定一個(gè)方法集,所有方法為接口類型就被認(rèn)為是該接口。
下面定義一個(gè)接口:
復(fù)制代碼 代碼如下:
type Notifier interface {
Notify() error
}
我們定義了一個(gè)叫做 Notifier 的接口并包含一個(gè) Notify 方法。當(dāng)一個(gè)接口只包含一個(gè)方法時(shí),按照 Go 語言的約定命名該接口時(shí)添加 -er 后綴。這個(gè)約定很有用,特別是接口和方法具有相同名字和意義的時(shí)候。
我們可以在接口中定義盡可能多的方法,不過在 Go 語言標(biāo)準(zhǔn)庫中,你很難找到一個(gè)接口包含兩個(gè)以上的方法。
實(shí)現(xiàn)接口
當(dāng)涉及到我們該怎么讓我們的類型實(shí)現(xiàn)接口時(shí),Go 語言是特別的一個(gè)。Go 語言不需要我們顯式的實(shí)現(xiàn)類型的接口。如果一個(gè)接口里的所有方法都被我們的類型實(shí)現(xiàn)了,那么我們就說該類型實(shí)現(xiàn)了該接口。
讓我們繼續(xù)之前的例子,定義一個(gè)函數(shù)來接受任意一個(gè)實(shí)現(xiàn)了接口 Notifier 的類型的值或者指針:
復(fù)制代碼 代碼如下:
func SendNotification(notify Notifier) error {
return notify.Notify()
}
SendNotification 函數(shù)調(diào)用 Notify 方法,這個(gè)方法被傳入函數(shù)的一個(gè)值或者指針實(shí)現(xiàn)。這樣一來一個(gè)函數(shù)就可以被用來執(zhí)行任意一個(gè)實(shí)現(xiàn)了該接口的值或者指針的指定的行為。
用我們的 User 類型來實(shí)現(xiàn)該接口并且傳入一個(gè) User 類型的值來調(diào)用 SendNotification 方法:
復(fù)制代碼 代碼如下:
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s%s>\n",
u.Name,
u.Email)
return nil
}
func main() {
user := User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
}
SendNotification(user)
}
// Output:
cannot use user (type User) as type Notifier in function argument:
User does not implement Notifier (Notify method has pointer receiver)
詳細(xì)代碼:http://play.golang.org/p/KG8-Qb7gqM
為什么編譯器不考慮我們的值是實(shí)現(xiàn)該接口的類型?接口的調(diào)用規(guī)則是建立在這些方法的接受者和接口如何被調(diào)用的基礎(chǔ)上。下面的是語言規(guī)范里定義的規(guī)則,這些規(guī)則用來說明是否我們一個(gè)類型的值或者指針實(shí)現(xiàn)了該接口:
1.類型 *T 的可調(diào)用方法集包含接受者為 *T 或 T 的所有方法集
這條規(guī)則說的是如果我們用來調(diào)用特定接口方法的接口變量是一個(gè)指針類型,那么方法的接受者可以是值類型也可以是指針類型。顯然我們的例子不符合該規(guī)則,因?yàn)槲覀儌魅?SendNotification 函數(shù)的接口變量是一個(gè)值類型。
1.類型 T 的可調(diào)用方法集包含接受者為 T 的所有方法
這條規(guī)則說的是如果我們用來調(diào)用特定接口方法的接口變量是一個(gè)值類型,那么方法的接受者必須也是值類型該方法才可以被調(diào)用。顯然我們的例子也不符合這條規(guī)則,因?yàn)槲覀?Notify 方法的接受者是一個(gè)指針類型。
語言規(guī)范里只有這兩條規(guī)則,我通過這兩條規(guī)則得出了符合我們例子的規(guī)則:
1.類型 T 的可調(diào)用方法集不包含接受者為 *T 的方法
我們碰巧趕上了我推斷出的這條規(guī)則,所以編譯器會(huì)報(bào)錯(cuò)。Notify 方法使用指針類型作為接受者而我們卻通過值類型來調(diào)用該方法。解決辦法也很簡單,我們只需要傳入 User 值的地址到 SendNotification 函數(shù)就好了:
復(fù)制代碼 代碼如下:
func main() {
user := User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
}
SendNotification(user)
}
// Output:
User: Sending User Email To AriesDevilariesdevil@xxoo.com>
詳細(xì)代碼:http://play.golang.org/p/kEKzyTfLjA
嵌入類型
結(jié)構(gòu)體類型可以包含匿名或者嵌入字段。也叫做嵌入一個(gè)類型。當(dāng)我們嵌入一個(gè)類型到結(jié)構(gòu)體中時(shí),該類型的名字充當(dāng)了嵌入字段的字段名。
下面定義一個(gè)新的類型然后把我們的 User 類型嵌入進(jìn)去:
復(fù)制代碼 代碼如下:
type Admin struct {
User
Level string
}
我們定義了一個(gè)新類型 Admin 然后把 User 類型嵌入進(jìn)去,注意這個(gè)不叫繼承而叫組合。 User 類型跟 Admin 類型沒有關(guān)系。
我們來改變一下 main 函數(shù),創(chuàng)建一個(gè) Admin 類型的變量并把變量的地址傳入 SendNotification 函數(shù)中:
復(fù)制代碼 代碼如下:
func main() {
admin := Admin{
User: User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
},
Level: "master",
}
SendNotification(admin)
}
// Output
User: Sending User Email To AriesDevilariesdevil@xxoo.com>
詳細(xì)代碼:http://play.golang.org/p/ivzzzk78TC
事實(shí)證明,我們可以 Admin 類型的一個(gè)指針來調(diào)用 SendNotification 函數(shù)?,F(xiàn)在 Admin 類型也通過來自嵌入的 User 類型的方法提升實(shí)現(xiàn)了該接口。
如果 Admin 類型包含了 User 類型的字段和方法,那么它們在結(jié)構(gòu)體中的關(guān)系是怎么樣的呢?
當(dāng)我們嵌入一個(gè)類型,這個(gè)類型的方法就變成了外部類型的方法,但是當(dāng)它被調(diào)用時(shí),方法的接受者是內(nèi)部類型(嵌入類型),而非外部類型。— Effective Go
因此嵌入類型的名字充當(dāng)著字段名,同時(shí)嵌入類型作為內(nèi)部類型存在,我們可以使用下面的調(diào)用方法:
復(fù)制代碼 代碼如下:
admin.User.Notify()
// Output
User: Sending User Email To AriesDevilariesdevil@xxoo.com>
詳細(xì)代碼:http://play.golang.org/p/0WL_5Q6mao
這兒我們通過類型名稱來訪問內(nèi)部類型的字段和方法。然而,這些字段和方法也同樣被提升到了外部類型:
復(fù)制代碼 代碼如下:
admin.Notify()
// Output
User: Sending User Email To AriesDevilariesdevil@xxoo.com>
詳細(xì)代碼:http://play.golang.org/p/2snaaJojRo
所以通過外部類型來調(diào)用 Notify 方法,本質(zhì)上是內(nèi)部類型的方法。
下面是 Go 語言中內(nèi)部類型方法集提升的規(guī)則:
給定一個(gè)結(jié)構(gòu)體類型 S 和一個(gè)命名為 T 的類型,方法提升像下面規(guī)定的這樣被包含在結(jié)構(gòu)體方法集中:
1.如果 S 包含一個(gè)匿名字段 T,S 和 *S 的方法集都包含接受者為 T 的方法提升。
這條規(guī)則說的是當(dāng)我們嵌入一個(gè)類型,嵌入類型的接受者為值類型的方法將被提升,可以被外部類型的值和指針調(diào)用。
1.對(duì)于 *S 類型的方法集包含接受者為 *T 的方法提升
這條規(guī)則說的是當(dāng)我們嵌入一個(gè)類型,可以被外部類型的指針調(diào)用的方法集只有嵌入類型的接受者為指針類型的方法集,也就是說,當(dāng)外部類型使用指針調(diào)用內(nèi)部類型的方法時(shí),只有接受者為指針類型的內(nèi)部類型方法集將被提升。
1.如果 S 包含一個(gè)匿名字段 *T,S 和 *S 的方法集都包含接受者為 T 或者 *T 的方法提升
這條規(guī)則說的是當(dāng)我們嵌入一個(gè)類型的指針,嵌入類型的接受者為值類型或指針類型的方法將被提升,可以被外部類型的值或者指針調(diào)用。
這就是語言規(guī)范里方法提升中僅有的三條規(guī)則,我根據(jù)這個(gè)推導(dǎo)出一條規(guī)則:
1.如果 S 包含一個(gè)匿名字段 T,S 的方法集不包含接受者為 *T 的方法提升。
這條規(guī)則說的是當(dāng)我們嵌入一個(gè)類型,嵌入類型的接受者為指針的方法將不能被外部類型的值訪問。這也是跟我們上面陳述的接口規(guī)則一致。
回答開頭的問題
現(xiàn)在我們可以寫程序來回答開頭提出的兩個(gè)問題了,首先我們讓 Admin 類型實(shí)現(xiàn) Notifier 接口:
復(fù)制代碼 代碼如下:
func (a *Admin) Notify() error {
log.Printf("Admin: Sending Admin Email To %s%s>\n",
a.Name,
a.Email)
return nil
}
Admin 類型實(shí)現(xiàn)的接口顯示一條 admin 方面的信息。當(dāng)我們使用 Admin 類型的指針去調(diào)用函數(shù) SendNotification 時(shí),這將幫助我們確定到底是哪個(gè)接口實(shí)現(xiàn)被調(diào)用了。
現(xiàn)在創(chuàng)建一個(gè) Admin 類型的值并把它的地址傳入 SendNotification 函數(shù),來看看發(fā)生了什么:
復(fù)制代碼 代碼如下:
func main() {
admin := Admin{
User: User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
},
Level: "master",
}
SendNotification(admin)
}
// Output
Admin: Sending Admin Email To AriesDevilariesdevil@xxoo.com>
詳細(xì)代碼:http://play.golang.org/p/JGhFaJnGpS
預(yù)料之中,Admin 類型的接口實(shí)現(xiàn)被 SendNotification 函數(shù)調(diào)用?,F(xiàn)在我們用外部類型來調(diào)用 Notify 方法會(huì)發(fā)生什么呢:
復(fù)制代碼 代碼如下:
admin.Notify()
// Output
Admin: Sending Admin Email To AriesDevilariesdevil@xxoo.com>
詳細(xì)代碼:http://play.golang.org/p/EGqK6DwBOi
我們得到了 Admin 類型的接口實(shí)現(xiàn)的輸出。User 類型的接口實(shí)現(xiàn)不被提升到外部類型了。
現(xiàn)在我們有了足夠的依據(jù)來回答問題了:
1.編譯器會(huì)因?yàn)槲覀兺瑫r(shí)有兩個(gè)接口實(shí)現(xiàn)而報(bào)錯(cuò)嗎?
不會(huì),因?yàn)楫?dāng)我們使用嵌入類型時(shí),類型名充當(dāng)了字段名。嵌入類型作為結(jié)構(gòu)體的內(nèi)部類型包含了自己的字段和方法,且具有唯一的名字。所以我們可以有同一接口的內(nèi)部實(shí)現(xiàn)和外部實(shí)現(xiàn)。
1.如果編譯器接受這樣的定義,那么當(dāng)接口調(diào)用時(shí)編譯器要怎么確定該使用哪個(gè)實(shí)現(xiàn)?
如果外部類型包含了符合要求的接口實(shí)現(xiàn),它將會(huì)被使用。否則,通過方法提升,任何內(nèi)部類型的接口實(shí)現(xiàn)可以直接被外部類型使用。
總結(jié)
在 Go 語言中,方法,接口和嵌入類型一起工作方式是獨(dú)一無二的。這些特性可以幫助我們像面向?qū)ο竽菢咏M織結(jié)構(gòu)然后達(dá)到同樣的目的,并且沒有其它復(fù)雜的東西。用本文中談到的語言特色,我們可以以極少的代碼來構(gòu)建抽象和可伸縮性的框架。
您可能感興趣的文章:- Go語言中嵌入C語言的方法
- golang官方嵌入文件到可執(zhí)行程序的示例詳解