Go 面向?qū)ο缶幊唐ㄈ和ㄟ^(guò)組合實(shí)現(xiàn)類的繼承和方法重寫
一、概述
在前面兩篇教程中,學(xué)院君已經(jīng)介紹了 Go 語(yǔ)言不像 Java、PHP 等支持面向編程的語(yǔ)言那樣,支持 class 之類的關(guān)鍵字來(lái)定義類,而是通過(guò) type 關(guān)鍵字結(jié)合基本類型或者結(jié)構(gòu)體來(lái)自定義類型系統(tǒng),此外,它也不支持通過(guò) extends 關(guān)鍵字來(lái)顯式定義類型之間的繼承關(guān)系。
所以,嚴(yán)格來(lái)說(shuō),Go 語(yǔ)言并不是一門面向?qū)ο缶幊陶Z(yǔ)言,至少不是面向?qū)ο缶幊痰淖罴堰x擇(Java 才是最根正苗紅的),不過(guò)我們可以基于它提供的一些特性來(lái)模擬實(shí)現(xiàn)面向?qū)ο缶幊獭?/p>
要實(shí)現(xiàn)面向?qū)ο缶幊?,就必須?shí)現(xiàn)面向?qū)ο缶幊痰娜筇匦裕悍庋b、繼承和多態(tài)。
二、封裝
首先是封裝,這一點(diǎn)我們?cè)?a target="_blank" textvalue="上篇教程" data-itemshowtype="0" tab="innerlink" data-linktype="2">上篇教程中已經(jīng)詳細(xì)介紹過(guò):將函數(shù)定義為歸屬某個(gè)自定義類型,這就等同于實(shí)現(xiàn)了類的成員方法,如果這個(gè)自定義類型是基于結(jié)構(gòu)體的,那么結(jié)構(gòu)體的字段可以看做是類的屬性。
三、繼承
然后是繼承,Go 雖然沒有直接提供繼承相關(guān)的語(yǔ)法實(shí)現(xiàn),但是我們通過(guò)組合的方式間接實(shí)現(xiàn)類似功能,所謂組合,就是將一個(gè)類型嵌入到另一個(gè)類型,從而構(gòu)建新的類型結(jié)構(gòu)。
傳統(tǒng)面向?qū)ο缶幊讨?,顯式定義繼承關(guān)系的弊端有兩個(gè):一個(gè)是導(dǎo)致類的層級(jí)越來(lái)越復(fù)雜,另一個(gè)是影響了類的擴(kuò)展性,很多軟件設(shè)計(jì)模式的理念就是通過(guò)組合來(lái)替代繼承提高類的擴(kuò)展性。
我們來(lái)看一個(gè)例子,現(xiàn)在有一個(gè) Animal 結(jié)構(gòu)體類型,它有一個(gè)屬性 Name 用于表示該動(dòng)物的名稱,以及三個(gè)成員方法,分別用來(lái)獲取動(dòng)物叫聲、喜歡的食物和動(dòng)物的名稱:
type Animal struct {
Name string
}
func (a Animal) Call() string {
return "動(dòng)物的叫聲..."
}
func (a Animal) FavorFood() string {
return "愛吃的食物..."
}
func (a Animal) GetName() string {
return a.Name
}
如果我們要定義一個(gè)繼承自該類型的子類 Dog,可以這么做:
type Dog struct {
Animal
}
這里,我們?cè)?Dog 結(jié)構(gòu)體類型中,嵌入了 Animal 這個(gè)類型,這樣一來(lái),我們就可以在 Dog 實(shí)例上訪問(wèn)所有 Animal 類型包含的屬性和方法:
func main() {
animal := Animal{"中華田園犬"}
dog := Dog{animal}
fmt.Println(dog.GetName())
fmt.Println(dog.Call())
fmt.Println(dog.FavorFood())
}
上述代碼的打印結(jié)果如下:
中華田園犬
動(dòng)物的叫聲...
愛吃的食物...
這就相當(dāng)于通過(guò)組合實(shí)現(xiàn)了類與類之間的繼承功能。
四、多態(tài)
此外,我們還可以通過(guò)在子類中定義同名方法來(lái)覆蓋父類方法的實(shí)現(xiàn),在面向?qū)ο缶幊讨羞@一術(shù)語(yǔ)叫做方法重寫,比如在上述 Dog 類型中,我們可以重寫 Call 方法和 FavorFood 方法的實(shí)現(xiàn)如下:
func (d Dog) FavorFood() string {
return "骨頭"
}
func (d Dog) Call() string {
return "汪汪汪"
}
當(dāng)我們?cè)賵?zhí)行 main 函數(shù)時(shí),直接在 Dog 實(shí)例上調(diào)用 Call 方法或 FavorFood 方法時(shí),調(diào)用的就是 Dog 類中定義的方法而不是 Animal 中定義的方法:

當(dāng)然,你可以可以像這樣繼續(xù)調(diào)用父類 Animal 中的方法:
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())
只不過(guò) Go 語(yǔ)言不同于 Java、PHP 等面向?qū)ο缶幊陶Z(yǔ)言,沒有專門提供引用父類實(shí)例的關(guān)鍵字罷了(super、parent 等),在 Go 語(yǔ)言中,設(shè)計(jì)哲學(xué)一切從簡(jiǎn),沒有一個(gè)多余的關(guān)鍵字,所有的調(diào)用都是所見即所得。
這種同一個(gè)方法在不同情況下具有不同的表現(xiàn)方式,就是多態(tài),在傳統(tǒng)面向?qū)ο缶幊讨?,多態(tài)還有另一個(gè)非常常見的使用場(chǎng)景 —— 類對(duì)接口的實(shí)現(xiàn),Go 語(yǔ)言也支持此功能,關(guān)于這一塊我們放到后面接口部分單獨(dú)介紹。
五、更多細(xì)節(jié)
可以看到,與傳統(tǒng)面向?qū)ο缶幊陶Z(yǔ)言的繼承機(jī)制不同,這種組合的實(shí)現(xiàn)方式更加靈活,我們不用考慮單繼承還是多繼承,你想要繼承哪個(gè)類型的方法,直接組合進(jìn)來(lái)就好了。
多繼承同名方法沖突處理
需要注意組合的不同類型之間包含同名方法,比如 Animal 和 Pet 都包含了 GetName 方法,如果子類 Dog 沒有重寫該方法,直接在 Dog 實(shí)例上調(diào)用的話會(huì)報(bào)錯(cuò):
...
type Pet struct {
Name string
}
func (p Pet) GetName() string {
return p.Name
}
type Dog struct {
Animal
Pet
}
...
func main() {
animal := Animal{"中華田園犬"}
pet := Pet{"寵物狗"}
dog := Dog{animal, pet}
fmt.Println(dog.GetName())
...
}
執(zhí)行上述代碼會(huì)報(bào)錯(cuò):
# command-line-arguments
chapter04/03-compose.go:49:17: ambiguous selector dog.GetName
除非你顯式指定調(diào)用哪個(gè)父類的方法:
fmt.Println(dog.Pet.GetName())
調(diào)整組合位置改變內(nèi)存布局
另外,我們還可以通過(guò)任意調(diào)整被組合類型的位置來(lái)改變類的內(nèi)存布局:
type Dog struct {
Animal
Pet
}
和
type Dog struct {
Pet
Animal
}
雖然上面兩個(gè) Dog 子類的功能一致,但是它們的內(nèi)存結(jié)構(gòu)不同。
繼承指針類型的屬性和方法
當(dāng)然,在 Go 語(yǔ)言中,你還可以以指針方式繼承某個(gè)類型的屬性和方法:
type Dog struct {
*Animal
}
這種情況下,除了傳入 Animal 實(shí)例的時(shí)候要傳入指針引用之外,其它調(diào)用無(wú)需修改:
func main() {
animal := Animal{"中華田園犬"}
pet := Pet{"寵物狗"}
dog := Dog{&animal, pet}
fmt.Println(dog.Animal.GetName())
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())
}
當(dāng)我們通過(guò)組合實(shí)現(xiàn)類之間的繼承時(shí),由于結(jié)構(gòu)體實(shí)例本身是值類型,如果傳入值字面量的話,實(shí)際上傳入的是結(jié)構(gòu)體實(shí)例的副本,對(duì)內(nèi)存耗費(fèi)更大,所以組合指針類型性能更好。
為組合類型設(shè)置別名
前面的示例調(diào)用父類方法時(shí)都直接引用的是組合類型(父類)的類型字面量,其實(shí),我們還可以像基本類型一樣,為其設(shè)置別名,方便引用:
type Dog struct {
animal *Animal
pet Pet
}
...
func main() {
animal := Animal{"中華田園犬"}
pet := Pet{"寵物狗"}
dog := Dog{&animal, pet}
// 通過(guò) animal 引用 Animal 類型實(shí)例
fmt.Println(dog.animal.GetName())
fmt.Print(dog.animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.animal.FavorFood())
fmt.Println(dog.FavorFood())
}
關(guān)于 Go 語(yǔ)言如何通過(guò)組合實(shí)現(xiàn)類與類之間的繼承和方法重寫,學(xué)院君就簡(jiǎn)單介紹到這里,下篇教程,我們一起來(lái)看看 Go 語(yǔ)言是如何管理類屬性和方法的可見性的。
本篇教程的源碼可以在 Github 代碼倉(cāng)庫(kù)獲?。篽ttps://github.com/nonfu/golang-tutorial/blob/main/chapter04/03-compose.go。
(本文完)
學(xué)習(xí)過(guò)程中有任何問(wèn)題,可以通過(guò)下面的評(píng)論功能或加入「Go 語(yǔ)言研習(xí)社」與學(xué)院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點(diǎn)擊頁(yè)面左下角閱讀原文鏈接查看最新更新的教程。

