概述
在 Go 語言中,如果一個結(jié)構(gòu)體和一個嵌入字段同時實(shí)現(xiàn)了相同的接口會發(fā)生什么呢?我們猜一下三热,可能有兩個問題:
- 編譯器會因為我們同時有兩個接口實(shí)現(xiàn)而報錯嗎鼓择?
- 如果編譯器接受這樣的定義,那么當(dāng)接口調(diào)用時編譯器要怎么確定該使用哪個實(shí)現(xiàn)就漾?
實(shí)現(xiàn)接口
當(dāng)涉及到我們該怎么讓我們的類型實(shí)現(xiàn)接口時呐能,Go 語言是特別的一個。Go 語言不需要我們顯式的實(shí)現(xiàn)類型的接口抑堡。如果一個接口里的所有方法都被我們的類型實(shí)現(xiàn)了摆出,那么我們就說該類型實(shí)現(xiàn)了該接口。
package main
import (
"log"
)
type User struct {
Name string
Email string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
user := User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
}
SendNotification(user)
}
實(shí)例源碼:http://play.golang.org/p/KG8-Qb7gqM
以上代碼輸出:
prog.go:34: cannot use user (type User) as type Notifier in argument to SendNotification: User does not implement Notifier (Notify method has pointer receiver)
為什么編譯器不考慮我們的值是實(shí)現(xiàn)該接口的類型首妖?接口的調(diào)用規(guī)則是建立在這些方法的接受者和接口如何被調(diào)用的基礎(chǔ)上偎漫。下面的是語言規(guī)范里定義的規(guī)則,這些規(guī)則用來說明是否我們一個類型的值或者指針實(shí)現(xiàn)了該接口:
- 類型 *T 的可調(diào)用方法集包含接受者為 *T 或 T 的所有方法集
這條規(guī)則說的是如果我們用來調(diào)用特定接口方法的接口變量是一個指針類型有缆,那么方法的接受者可以是值類型也可以是指針類型象踊。顯然我們的例子不符合該規(guī)則,因為我們傳入 SendNotification 函數(shù)的接口變量是一個值類型棚壁。 - 類型 T 的可調(diào)用方法集包含接受者為 T 的所有方法
這條規(guī)則說的是如果我們用來調(diào)用特定接口方法的接口變量是一個值類型杯矩,那么方法的接受者必須也是值類型該方法才可以被調(diào)用。顯然我們的例子也不符合這條規(guī)則袖外,因為我們 Notify 方法的接受者是一個指針類型史隆。
語言規(guī)范里只有這兩條規(guī)則,我通過這兩條規(guī)則得出了符合我們例子的規(guī)則: - 類型 T 的可調(diào)用方法集不包含接受者為 *T 的方法
我們碰巧趕上了我推斷出的這條規(guī)則在刺,所以編譯器會報錯逆害。Notify 方法使用指針類型作為接受者而我們卻通過值類型來調(diào)用該方法头镊。解決辦法也很簡單蚣驼,我們只需要傳入 User 值的地址到 SendNotification 函數(shù)就好了:
package main
import (
"log"
)
type User struct {
Name string
Email string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
user := &User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
}
SendNotification(user)
}
嵌入類型
結(jié)構(gòu)體類型可以包含匿名或者嵌入字段。也叫做嵌入一個類型相艇。當(dāng)我們嵌入一個類型到結(jié)構(gòu)體中時颖杏,該類型的名字充當(dāng)了嵌入字段的字段名。
package main
import (
"log"
)
type User struct {
Name string
Email string
}
type Admin struct {
User
Level string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
admin := &Admin{
User: User{
Name: "AriesDevil",
Email: "ariesdevil@xxoo.com",
},
Level: "super",
}
SendNotification(admin)
}
詳細(xì)代碼:http://play.golang.org/p/ivzzzk78TC
事實(shí)證明坛芽,我們可以 Admin 類型的一個指針來調(diào)用 SendNotification 函數(shù)×舸ⅲ現(xiàn)在 Admin 類型也通過來自嵌入的 User 類型的方法提升實(shí)現(xiàn)了該接口。
如果 Admin 類型包含了 User 類型的字段和方法咙轩,那么它們在結(jié)構(gòu)體中的關(guān)系是怎么樣的呢获讳?
當(dāng)我們嵌入一個類型,這個類型的方法就變成了外部類型的方法活喊,但是當(dāng)它被調(diào)用時丐膝,方法的接受者是內(nèi)部類型(嵌入類型),而非外部類型漆撞∶墩常— Effective Go
因此嵌入類型的名字充當(dāng)著字段名柒巫,同時嵌入類型作為內(nèi)部類型存在响疚,我們可以使用下面的調(diào)用方法:
admin.User.Notify()
// OutputUser: Sending User Email To AriesDevil<ariesdevil@xxoo.com>
這兒我們通過類型名稱來訪問內(nèi)部類型的字段和方法宵蛀。然而西乖,這些字段和方法也同樣被提升到了外部類型:
admin.Notify()
// OutputUser: Sending User Email To AriesDevil<ariesdevil@xxoo.com>
所以通過外部類型來調(diào)用 Notify 方法敷燎,本質(zhì)上是內(nèi)部類型的方法谣殊。
下面是 Go 語言中內(nèi)部類型方法集提升的規(guī)則:
給定一個結(jié)構(gòu)體類型 S 和一個命名為 T 的類型凛俱,方法提升像下面規(guī)定的這樣被包含在結(jié)構(gòu)體方法集中:
- 如果 S 包含一個匿名字段 T紊馏,S 和 *S 的方法集都包含接受者為 T 的方法提升。
這條規(guī)則說的是當(dāng)我們嵌入一個類型最冰,嵌入類型的接受者為值類型的方法將被提升瘦棋,可以被外部類型的值和指針調(diào)用。
- 對于 *S 類型的方法集包含接受者為 *T 的方法提升
這條規(guī)則說的是當(dāng)我們嵌入一個類型暖哨,可以被外部類型的指針調(diào)用的方法集只有嵌入類型的接受者為指針類型的方法集赌朋,也就是說,當(dāng)外部類型使用指針調(diào)用內(nèi)部類型的方法時篇裁,只有接受者為指針類型的內(nèi)部類型方法集將被提升沛慢。
- 如果 S 包含一個匿名字段 *T,S 和 *S 的方法集都包含接受者為 T 或者 *T 的方法提升
這條規(guī)則說的是當(dāng)我們嵌入一個類型的指針达布,嵌入類型的接受者為值類型或指針類型的方法將被提升团甲,可以被外部類型的值或者指針調(diào)用。
這就是語言規(guī)范里方法提升中僅有的三條規(guī)則黍聂,我根據(jù)這個推導(dǎo)出一條規(guī)則:
如果 S 包含一個匿名字段 T躺苦,S 的方法集不包含接受者為 *T 的方法提升。
這條規(guī)則說的是當(dāng)我們嵌入一個類型产还,嵌入類型的接受者為指針的方法將不能被外部類型的值訪問匹厘。這也是跟我們上面陳述的接口規(guī)則一致。
回答開頭的問題
現(xiàn)在我們可以寫程序來回答開頭提出的兩個問題了脐区,首先我們讓 Admin 類型實(shí)現(xiàn) Notifier 接口:
Admin 類型實(shí)現(xiàn)的接口顯示一條 admin 方面的信息愈诚。當(dāng)我們使用 Admin 類型的指針去調(diào)用函數(shù) SendNotification 時,這將幫助我們確定到底是哪個接口實(shí)現(xiàn)被調(diào)用了牛隅。
現(xiàn)在創(chuàng)建一個 Admin 類型的值并把它的地址傳入 SendNotification 函數(shù)炕柔,來看看發(fā)生了什么:
package main
import (
"log"
)
type User struct {
Name string
Email string
}
type Admin struct {
User
Level string
}
func (u *User) Notify() error {
log.Printf("User: Sending User Email To %s<%s>\n",
u.Name,
u.Email)
return nil
}
//func (a *Admin) Notify() error {
// log.Printf("Admin: Sending Admin Email To %s<%s>\n",
// a.Name,
// a.Email)
//
// return nil
//}
type Notifier interface {
Notify() error
}
func SendNotification(notify Notifier) error {
return notify.Notify()
}
func main() {
admin := &Admin{}
admin.Notify()
admin.User.Notify()
SendNotification(admin)
}
源代碼實(shí)例分享:https://play.golang.org/p/JGhFaJnGpS
編譯器會因為我們同時有兩個接口實(shí)現(xiàn)而報錯嗎?
不會媒佣,因為當(dāng)我們使用嵌入類型時匕累,類型名充當(dāng)了字段名。嵌入類型作為結(jié)構(gòu)體的內(nèi)部類型包含了自己的字段和方法默伍,且具有唯一的名字欢嘿。所以我們可以有同一接口的內(nèi)部實(shí)現(xiàn)和外部實(shí)現(xiàn)授霸。如果編譯器接受這樣的定義,那么當(dāng)接口調(diào)用時編譯器要怎么確定該使用哪個實(shí)現(xiàn)际插?
如果外部類型包含了符合要求的接口實(shí)現(xiàn)碘耳,它將會被使用。否則框弛,通過方法提升辛辨,任何內(nèi)部類型的接口實(shí)現(xiàn)可以直接被外部類型使用。總結(jié)
在 Go 語言中瑟枫,方法斗搞,接口和嵌入類型一起工作方式是獨(dú)一無二的。這些特性可以幫助我們像面向?qū)ο竽菢咏M織結(jié)構(gòu)然后達(dá)到同樣的目的慷妙,并且沒有其它復(fù)雜的東西僻焚。用本文中談到的語言特色,我們可以以極少的代碼來構(gòu)建抽象和可伸縮性的框架膝擂。
轉(zhuǎn)自:http://www.jb51.net/article/56831.htm