這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
確切地說,Go語言也提供了繼承,但是採用了組合的文法,所以我們將其稱為匿名組合:
type Base struct {
聽聽聽聽Name string
}
func (base *Base) Foo() {...}
func (base *Base) Bar() {...}
type Foo struct {
聽聽聽聽Base
聽聽聽聽...
}
func (foo *Foo) Bar() {
聽聽聽聽foo.Base.Bar()
聽聽聽聽...
}
以上代碼定義了一個Base類(實現了Foo()和Bar()兩個成員方法),然後定義了一個Foo類,該類從Base類“繼承”並改寫了Bar()方法(該方法實現時先調用了基類的Bar()方法)。
在“衍生類別”Foo沒有寫“基類”Base的成員方法時,相應的方法就被“繼承”,例如在上面的例子中,調用foo.Foo()和調用foo.Base.Foo()效果一致。
與其他語言不同,Go語言很清晰地告訴你的記憶體布局是怎樣的。此外,在Go語言中你還可以隨心所欲地修改記憶體布局,如:
type Foo struct {
聽聽聽聽...//其他成員
聽聽聽聽Base
}
這段代碼從語義上來說,和上面的例子並無不同,但記憶體布局發生了改變。“基類”Base的資料放在了“衍生類別”Foo的最後。
另外,在Go語言中,你還可以以指標方式從一類類型“派生”:
type Foo struct {
聽聽聽聽*Base
聽聽聽聽...
}
這段Go代碼仍然有“派生”的效果,只是Foo建立執行個體的時候,需要外部提供一個Base類執行個體的指標。
在C++語言中其實也有類似的功能,那就是虛基類,但是它非常讓人難以理解,一般C++的開發人員都會遺忘這個特性。相比之下,Go語言以一種非常容易理解的方式提供了一些原本期望用虛基類才能解決的設計難題。
在Go語言官方網站提供的Effective Go中曾提到匿名組合的一個小价值,值得在這裡再提一下。首先我們可以定義如下的類型,它匿名組合了一個log.logger指標:
type Job struct {
聽聽聽聽Command string
聽聽聽聽*log.Logger
}
在合適的賦值後,我們在Job類型的所有成員方法中可以很舒適地借用所有log.Logger提供的方法。比如如下的寫法:
func (job *Job) Start() {
聽聽聽聽job.Log("starting now...")
聽聽聽聽...//做一些事情
聽聽聽聽job.Log("started.")
}
對於Job的實現者來說,他甚至根本就不用意識到log.logger類型的存在,這就是匿名組合的魅力所在。在實際工作中,只有合理利用才能最大發揮這個功能的價值。
需要注意的是,不管是非匿名的類型組合還是匿名組合,被組合的類型所包含的方法雖然都升級了外部這個組合類別型的方法,但其實它們被組合的方法調用時接收者並沒有改變。比如上面這個Job例子,即使組合後調用的方式變成了job.Log(...),但Log函數的接收者仍然是log.Logger指標,因此在Log中不可能訪問到job的其他成員方法和變數。
這其實也很容易理解,畢竟被組合的類型並不知道自己會被什麼類型組合,當然就沒法在實現方法時去使用那個未知的“組合者”的功能了。
另外,我們必須關注一下介面組合中的名字衝突問題,比如如下的組合:
type X struct {
聽聽聽聽Name string
}
type Y struct {
聽聽聽聽X
聽聽聽聽Name string
}
組合的類型和被組合的類型都包含一個Name成員,會不會有問題呢?答案是否定的。所有的Y類型的Name成員的訪問都只會訪問到最外層的那個Name變數,X.Name變數相當於被隱藏起來了。
那麼下面這樣的情境呢:
type Logger struct {
聽聽聽聽level int
}
type Y struct {
聽聽聽聽*Logger
聽聽聽聽Name string
聽聽聽聽*log.Logger
}
顯然這裡會有問題。因為之前已經提到過,匿名組合類別型相當於以其類型名稱(去掉包名部分)作為成員變數的名字。按此規則,Y類型中就相當於存在兩個名為Logger的成員,雖然類型不同。因此,我們預期會收到編譯錯誤。
有意思的是,這個編譯錯誤並不是一定會發生的。假如這兩個Logger在定義後再也沒有被用過,那麼編譯器將直接忽略掉這個衝突問題,直至開發人員開始使用其中的某個Logger。