本文最初發表在我的個人博客,查看原文,獲得更好的閱讀體驗
在像Java這種語言中,有子類(或者繼承)的概念,通過繼承複用已有的功能或屬性,與繼承不同,Go中使用組合的方式來完成已有實現的複用,這種做法稱爲內嵌。具體來說,就是將已定義類型內嵌到結構體或接口中完成組合。
一 接口內嵌
接口內嵌非常簡單。例如標準庫中的io.Reader
和io.Writer
接口,它們的定義如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
我們當然可以聲明一個新的接口來顯示的包含上述的Read
和Write
方法:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
但是,更好的做法是將已有的接口內嵌到新的接口中,就像標準庫io.ReadWriter
所做的這樣:
// ReadWriter 接口整合了 Reader 和 Writer 接口。
type ReadWriter interface {
Reader
Writer
}
現在,ReadWriter
同時擁有了Reader
和Writer
的功能。
需要注意的是,內嵌的接口中不能包含重複的方法,而且接口中只能嵌套接口。
例如以下錯誤示例
:
package main
import "fmt"
func main() {
fmt.Println("test")
}
type A interface {
M1()
M2()
}
type B interface {
M2()
M3()
}
type C interface {
A
B // 編譯錯誤:duplicate method M2
Counter // 編譯錯誤:interface contains embedded non-interface Counter
}
type Counter uint
上述接口C
中包含的兩個子接口A
和B
中都有方法M2
,而Counter
本身不是接口,所以編譯會報錯。
接口不能直接或間接嵌套自身。
二 結構內嵌
結構中,同樣可以內嵌其他類型。例如標準庫bufio
包中有bufio.Reader
和bufio.Writer
兩個結構體類型,分別實現了io.Reader
和io.Writer
接口,而結構bufio.ReadWriter
則通過整合bufio.Reader
和bufio.Writer
到自身,實現了帶緩衝的reader/writer
:
// ReadWriter 存儲了指向 Reader 和 Writer 的指針。
// 它實現了 io.ReadWriter。
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
注意,上述的bufio.ReadWriter
只列出了結構類型,並未給予它們字段名。內嵌字段是結構指針的形式,所以使用前需要初始化爲有效的指針。當然,我們也可以這麼來定義ReadWriter
:
// 一個不好的示例
type ReadWriter struct {
reader *Reader
writer *Writer
}
但是如果這麼寫的話,我們就得提供轉發的方法來滿足io
中的接口:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
而通過直接內嵌結構體,我們就能避免這種繁瑣。內嵌類型的方法可以直接引用,這意味着bufio.ReadWriter
不僅包括bufio.Reader
和bufio.Writer
的方法,它還同時滿足下列三個接口:io.Reader
、io.Writer
以及io.ReadWriter
,在Go中,這種做法被稱爲晉升
(promoted
,詳見下文)。
還有種區分內嵌與子類(或者繼承)的重要手段。當內嵌一個類型時,該類型的方法會成爲外部類型的方法,但當它們被調用時,該方法的接收者仍然是內部類型,而非外部的。上述示例中,當bufio.ReadWriter
的Read
方法被調用時,它與之前寫的轉發方法具有同樣的效果;接收者是ReadWriter
的reader
字段,而非ReadWriter
本身。
若我們需要直接引用內嵌字段,可以忽略包限定名,直接將該字段的類型名作爲字段名。
2.1 內嵌字段
前面已經看到將其他類型內嵌到結構中的示例。這種使用類型聲明但沒有顯式字段名稱的字段稱爲內嵌字段
。必須將內嵌字段指定爲類型名稱T
或指向非接口類型名稱*T
的指針,並且T
本身不是指針類型。非限定類型名稱充當字段名稱。
// 類型M有四個內嵌字段:T1, *T2, P.T3 還有 *P.T4
type M struct {
T1 // 字段名爲 T1
*T2 // 字段名爲 T2
P.T3 // 字段名爲 T3
*P.T4 // 字段名爲 T4
x, y int // 字段名爲 x 和 y
}
以下字段聲明不合法,因爲結構中的字段名稱需要唯一:
struct {
T // 與內嵌字段 *T 和 *P.T 衝突
*T // 與內嵌字段 T 和 *P.T 衝突
*P.T // 與內嵌字段 T 和 *T 衝突
}
2.2 晉升(Promoted)
如前文所講,對於結構x
中的內嵌字段或方法f
,如果x.f
是一個可以表示字段或方法的合法選擇器,則稱其爲晉升。晉升的字段除了不能在結構的複合字面量中表示字段名之外,與普通字段無異。
給定一個結構類型S
和一個已定義類型T
,晉升的方法會包含在結構的方法集中,只要滿足以下條件:
- 如果
S
包含內嵌字段T
,則S
和*S
的方法集均包含含有接收器T
的晉升方法。*S
的方法集還包含含有接收器*T
的晉升方法。 - 如果
S
包含內嵌字段*T
,則S
和*S
的方法集均包含含有接收器T
或*T
的晉升方法。
三 命名衝突問題
內嵌類型會引入命名衝突的問題,但解決規則卻很簡單。
首先,字段或方法X
會隱藏該類型中更深層嵌套的其它項X
。如下面示例中類型C
與類型B
中的字段F3
;類型C
中的字段F5
與類型B
中的方法F5
。
其次,若相同的嵌套層級上出現同名衝突,通常會產生一個錯誤。例如類型C
中已經有一個字段B
,則試圖再爲類型C
定義一個方法B
就會出錯。然而,若重名永遠不會在該類型定義之外的程序中使用,那就不會出錯,例如C
由A
和B
間接引入兩個同名字段F2
,但只要不調用v.F2
,就不會產生錯誤。這種限定能夠在外部嵌套類型發生修改時提供某種保護。因此,就算添加的字段與另一個子類型中的字段相沖突,只要這兩個相同的字段永遠不會被使用就沒問題。
注:重名定義發生在最外層嵌套時,定義即會出錯,其他更深入的嵌套在定義時不會出錯,但使用時會出錯。參考示例中的方法B(最外層重名)與字段F2(第二層重名)。
命名衝突示例:
package main
import "fmt"
func main() {
v := C{A{1, 2}, B{"test", 3, 4}, 5, 6}
fmt.Printf("%v,%T\n", v, v)
fmt.Println(v.B.F3, v.F3) // 結果:3 5。C中的字段F3覆蓋了B中的字段F3
fmt.Println(v.F5) // 結果:6。C中的F5會覆蓋內嵌類型B中的F5,故F5是一個字段,不是方法
// v.F5() // 編譯錯誤,方法被覆蓋掉了: cannot call non-function v.F5 (type int),
v.F6() // 可以直接調用內嵌類型B中的方法
v.F7() // C中的方法F7覆蓋了B中的方法F7
v.B.F7() // 由於被覆蓋,需要通過字段B間接調用B的方法
// fmt.Println(v.F2) // 編譯錯誤,分不清F2到底是指的A中的還是B中的:ambiguous selector v.F2
fmt.Println(v.A.F2) // 間接調用沒問題
fmt.Println(v.B.F2) // 間接調用沒問題
}
type A struct {
F1 int
F2 int
}
type B struct {
F2 string
F3 int
F4 int
}
type C struct {
A
B
F3 int
F5 int
}
func (b B) F5() {
fmt.Println("printed from B's method F5().")
}
func (b B) F6() {
fmt.Println("printed from B's method F6().")
}
func (b B) F7() {
fmt.Println("printed from B's method F7().")
}
func (c C) F7() {
fmt.Println("printed from C's method F7().")
}
// 編譯錯誤:type C has both field and method named B
// func (c C) B() {
// fmt.Println("printed from C's method B().")
// }
四 方法的繼承性
我們已經知道,已定義的類型可以有對應的方法。但是,新定義的類型並不會從創建它的非接口類型中繼承任何方法。接口類型或複合類型(例如內嵌)元素的方法集則保持不變:
// Mutex 是一個有兩個方法的數據類型:Lock 和 Unlock
type Mutex struct { /* Mutex fields */ }
func (m *Mutex) Lock() { /* Lock implementation */ }
func (m *Mutex) Unlock() { /* Unlock implementation */ }
// NewMutex和Mutex具有相同的結構(注意是不同的類型),但它的方法集是空的。
type NewMutex Mutex
// PtrMutex的底層類型*Mutex的方法集保持不變,但PtrMutex的方法集是空的。
type PtrMutex *Mutex
// *PrintableMutex 的方法集包含它的內嵌字段Mutex的方法集Lock 和 Unlock
type PrintableMutex struct {
Mutex
}
// crypto.cipher.Block
type Block interface {
// BlockSize returns the cipher's block size.
BlockSize() int
// Encrypt encrypts the first block in src into dst.
// Dst and src must overlap entirely or not at all.
Encrypt(dst, src []byte)
// Decrypt decrypts the first block in src into dst.
// Dst and src must overlap entirely or not at all.
Decrypt(dst, src []byte)
}
// MyBlock是一個接口類型,具有與Block一樣的方法集
// MyBlock is an interface type that has the same method set as Block.
type MyBlock Block
五 選擇器
對於表達式x
(不是包名),如下選擇器表達式:
x.f
表示值x
(或者*x
)的方法或字段。標識符f
被稱爲選擇器,選擇器不能爲空白標識符。f
的類型即選擇器表達式的類型。
選擇器f
可以表示爲類型T
的字段或方法,或者它可以指代嵌套的嵌入字段T
的字段或方法f
。遍歷到f
的嵌入字段的數量在T
中稱爲其深度。在T
中聲明的字段或方法f
的深度爲零。在T
中的嵌入字段A
中聲明的字段或方法f
的深度是A
中的f
的深度加1
。
以下規則適用於選擇器:
- 對於類型爲
T
或*T
的值x
,其中T
不是指針或接口類型,x.f
表示在T
中最淺的深度處的字段或方法f
:如果沒有一個具有最淺深度的f
,則選擇器表達式是非法的。 - 對於接口類型
I
的值x
,x.f
表示動態值x
的實際方法f
。如果在I
的方法集中沒有名稱爲f
的方法,則選擇器表達式是非法的。 - 一個例外情況是,如果
x
的類型是已定義的指針類型,而且(*x).f
是表示字段(而不是方法)的有效選擇器表達式,則x.f
是(*x).f
的簡寫。 - 除此之外,所有其他情況下,
x.f
都是非法的。 - 如果
x
是指針類型且值爲nil
,且x.f
表示結構字段,則爲x.f
賦值或對其求值會導致運行時panic。 - 如果
x
是接口類型且值爲nil
,則調用方法x.f
或對其求值會導致運行時panic。
參考:
https://golang.org/doc/effective_go.html#embedding
https://golang.org/ref/spec#Struct_types
https://golang.org/ref/spec#Type_declarations
https://golang.org/ref/spec#Selectors