這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
轉載:http://se77en.cc/2014/05/05/methods-interfaces-and-embedded-types-in-golang/
概述
在 Go 語言中,如果一個結構體和一個嵌入欄位同時實現了相同的介面會發生什麼呢?我們猜一下,可能有兩個問題:
- 編譯器會因為我們同時有兩個介面實現而報錯嗎?
- 如果編譯器接受這樣的定義,那麼當介面調用時編譯器要怎麼確定該使用哪個實現?
在寫了一些測試代碼並認真深入的讀了一下標準之後,我發現了一些有意思的東西,而且覺得很有必要分享出來,那麼讓我們先從 Go 語言中的方法開始說起。
方法
Go 語言中同時有函數和方法。一個方法就是一個包含了接受者的函數,接受者可以是命名類型或者結構體類型的一個值或者是一個指標。所有給定類型的方法屬於該類型的方法集。
下面定義一個結構體類型和該類型的一個方法:
type User struct { Name string Email string}func (u User) Notify() error
首先我們定義了一個叫做 User
的結構體類型,然後定義了一個該類型的方法叫做 Notify
,該方法的接受者是一個 User
類型的值。要調用 Notify
方法我們需要一個 User
類型的值或者指標:
// User 類型的值可以調用接受者是值的方法damon := User{"AriesDevil", "ariesdevil@xxoo.com"}damon.Notify()// User 類型的指標同樣可以調用接受者是值的方法alimon := &User{"A-limon", "alimon@ooxx.com"}alimon.Notify()
在這個例子中當我們使用指標時,Go 調整和解引用指標使得調用可以被執行。注意,當接受者不是一個指標時,該方法操作對應接受者的值的副本(意思就是即使你使用了指標調用函數,但是函數的接受者是實值型別,所以函數內部操作還是對副本的操作,而不是指標操作,參見:http://play.golang.org/p/DBhWU0p1Pv)。
我們可以修改 Notify
方法,讓它的接受者使用指標類型:
func (u *User) Notify() error
再來一次之前的調用(注意:當接受者是指標時,即使用實值型別調用那麼函數內部也是對指標的操作,參見:http://play.golang.org/p/SYBb4xPfPh):
// User 類型的值可以調用接受者是指標的方法damon := User{"AriesDevil", "ariesdevil@xxoo.com"}damon.Notify()// User 類型的指標同樣可以調用接受者是指標的方法alimon := &User{"A-limon", "alimon@ooxx.com"}alimon.Notify()
如果你不清楚到底什麼時候該使用值,什麼時候該使用指標作為接受者,你可以去看一下這篇介紹。這篇文章同時還包含了社區約定的接受者該如何命名。
介面
Go 語言中的介面很特別,而且提供了難以置信的一系列靈活性和抽象性。它們指定一個特定類型的值和指標表現為特定的方式。從語言角度看,介面是一種類型,它指定一個方法集,所有方法為介面類型就被認為是該介面。
下面定義一個介面:
type Notifier interface { Notify() error}
我們定義了一個叫做 Notifier
的介面並包含一個 Notify
方法。當一個介面只包含一個方法時,按照 Go 語言的約定命名該介面時添加 -er
尾碼。這個約定很有用,特別是介面和方法具有相同名字和意義的時候。
我們可以在介面中定義儘可能多的方法,不過在 Go 語言標準庫中,你很難找到一個介面包含兩個以上的方法。
實現介面
當涉及到我們該怎麼讓我們的類型實現介面時,Go 語言是特別的一個。Go 語言不需要我們顯式的實作類別型的介面。如果一個介面裡的所有方法都被我們的類型實現了,那麼我們就說該類型實現了該介面。
讓我們繼續之前的例子,定義一個函數來接受任意一個實現了介面 Notifier
的類型的值或者指標:
func SendNotification(notify Notifier) error { return notify.Notify()}
SendNotification
函數調用 Notify
方法,這個方法被傳入函數的一個值或者指標實現。這樣一來一個函數就可以被用來執行任意一個實現了該介面的值或者指標的指定的行為。
用我們的 User
類型來實現該介面並且傳入一個 User
類型的值來調用 SendNotification
方法:
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)
詳細代碼:http://play.golang.org/p/KG8-Qb7gqM
為什麼編譯器不考慮我們的值是實現該介面的類型?介面的調用規則是建立在這些方法的接受者和介面如何被調用的基礎上。下面的是語言規範裡定義的規則,這些規則用來說明是否我們一個類型的值或者指標實現了該介面:
- 類型
*T
的可調用方法集包含接受者為 *T
或 T
的所有方法集
這條規則說的是如果我們用來調用特定介面方法的介面變數是一個指標類型,那麼方法的接受者可以是實值型別也可以是指標類型。顯然我們的例子不符合該規則,因為我們傳入 SendNotification
函數的介面變數是一個實值型別。
- 類型
T
的可調用方法集包含接受者為 T
的所有方法
這條規則說的是如果我們用來調用特定介面方法的介面變數是一個實值型別,那麼方法的接受者必須也是實值型別該方法才可以被調用。顯然我們的例子也不符合這條規則,因為我們 Notify
方法的接受者是一個指標類型。
語言規範裡只有這兩條規則,我通過這兩條規則得出了符合我們例子的規則:
- 類型
T
的可調用方法集不包含接受者為 *T
的方法
我們碰巧趕上了我推斷出的這條規則,所以編譯器會報錯。Notify
方法使用指標類型作為接受者而我們卻通過實值型別來調用該方法。解決辦法也很簡單,我們只需要傳入 User
值的地址到 SendNotification
函數就好了:
func main() { user := &User{ Name: "AriesDevil", Email: "ariesdevil@xxoo.com", } SendNotification(user)}// Output:User: Sending User Email To AriesDevil
詳細代碼:http://play.golang.org/p/kEKzyTfLjA
嵌入類型
結構體類型可以包含匿名或者嵌入欄位。也叫做嵌入一個類型。當我們嵌入一個類型到結構體中時,該類型的名字充當了嵌入欄位的欄位名。
下面定義一個新的類型然後把我們的 User
類型嵌入進去:
type Admin struct { User Level string}
我們定義了一個新類型 Admin
然後把 User
類型嵌入進去,注意這個不叫繼承而叫組合。 User
類型跟 Admin
類型沒有關係。
我們來改變一下 main
函數,建立一個 Admin
類型的變數並把變數的地址傳入 SendNotification
函數中:
func main() { admin := &Admin{ User: User{ Name: "AriesDevil", Email: "ariesdevil@xxoo.com", }, Level: "master", } SendNotification(admin)}// OutputUser: Sending User Email To AriesDevil
詳細代碼:http://play.golang.org/p/ivzzzk78TC
事實證明,我們可以 Admin
類型的一個指標來調用 SendNotification
函數。現在 Admin
類型也通過來自嵌入的 User
類型的方法提升實現了該介面。
如果 Admin
類型包含了 User
類型的欄位和方法,那麼它們在結構體中的關係是怎麼樣的呢?
當我們嵌入一個類型,這個類型的方法就變成了外部類型的方法,但是當它被調用時,方法的接受者是內部類型(嵌入類型),而非外部類型。— Effective Go
因此嵌入類型的名字充當著欄位名,同時嵌入類型作為內部類型存在,我們可以使用下面的調用方法:
admin.User.Notify()// OutputUser: Sending User Email To AriesDevil
詳細代碼:http://play.golang.org/p/0WL_5Q6mao
這兒我們通過類型名稱來訪問內部類型的欄位和方法。然而,這些欄位和方法也同樣被提升到了外部類型:
admin.Notify()// OutputUser: Sending User Email To AriesDevil
詳細代碼:http://play.golang.org/p/2snaaJojRo
所以通過外部類型來調用 Notify
方法,本質上是內部類型的方法。
下面是 Go 語言中內部類型方法集提升的規則:
給定一個結構體類型 S
和一個命名為 T
的類型,方法提升像下面規定的這樣被包含在結構體方法集中:
- 如果
S
包含一個匿名欄位 T
,S
和 *S
的方法集都包含接受者為 T
的方法提升。
這條規則說的是當我們嵌入一個類型,嵌入類型的接受者為實值型別的方法將被提升,可以被外部類型的值和指標調用。
- 對於
*S
類型的方法集包含接受者為 *T
的方法提升
這條規則說的是當我們嵌入一個類型,可以被外部類型的指標調用的方法集只有嵌入類型的接受者為指標類型的方法集,也就是說,當外部類型使用指標調用內部類型的方法時,只有接受者為指標類型的內部類型方法集將被提升。
- 如果
S
包含一個匿名欄位 *T
,S
和 *S
的方法集都包含接受者為 T
或者 *T
的方法提升
這條規則說的是當我們嵌入一個類型的指標,嵌入類型的接受者為實值型別或指標類型的方法將被提升,可以被外部類型的值或者指標調用。
這就是語言規範裡方法提升中僅有的三條規則。
回答開頭的問題
現在我們可以寫程式來回答開頭提出的兩個問題了,首先我們讓 Admin
類型實現 Notifier
介面:
func (a *Admin) Notify() error { log.Printf("Admin: Sending Admin Email To %s<%s>\n", a.Name, a.Email) return nil}
Admin
類型實現的介面顯示一條 admin 方面的資訊。當我們使用 Admin
類型的指標去調用函數 SendNotification
時,這將協助我們確定到底是哪個介面實現被調用了。
現在建立一個 Admin
類型的值並把它的地址傳入 SendNotification
函數,來看看發生了什麼:
func main() { admin := &Admin{ User: User{ Name: "AriesDevil", Email: "ariesdevil@xxoo.com", }, Level: "master", } SendNotification(admin)}// OutputAdmin: Sending Admin Email To AriesDevil
詳細代碼:http://play.golang.org/p/JGhFaJnGpS
預料之中,Admin
類型的介面實現被 SendNotification
函數調用。現在我們用外部類型來調用 Notify
方法會發生什麼呢:
admin.Notify()// OutputAdmin: Sending Admin Email To AriesDevil
詳細代碼:http://play.golang.org/p/EGqK6DwBOi
我們得到了 Admin
類型的介面實現的輸出。User
類型的介面實現不被提升到外部類型了。
現在我們有了足夠的依據來回答問題了:
不會,因為當我們使用嵌入類型時,類型名充當了欄位名。嵌入類型作為結構體的內部類型包含了自己的欄位和方法,且具有唯一的名字。所以我們可以有同一介面的內部實現和外部實現。
- 如果編譯器接受這樣的定義,那麼當介面調用時編譯器要怎麼確定該使用哪個實現?
如果外部類型包含了符合要求的介面實現,它將會被使用。否則,通過方法提升,任何內部類型的介面實現可以直接被外部類型使用。
總結
在 Go 語言中,方法,介面和嵌入類型一起工作方式是獨一無二的。這些特性可以協助我們像物件導向那樣組織圖然後達到同樣的目的,並且沒有其它複雜的東西。用本文中談到的語言特色,我們可以以極少的代碼來構建抽象和延展性的架構。