大家好,今天是鐵人賽第十三天。在前兩天內容中,我們了解go語言的結構可以封裝資料,以及定義方法。至於今天我們要來談的是,結構該如何共享程式碼。
在物件導向程式中,通常會用繼承來共享上層元件的程式碼。然而,go語言沒有繼承的特性,但我們能用組合的方式來共享程式碼。不僅如此,go語言還提供一種優於組合的語法特性,稱作內嵌。
先來談談我所知道的組合,大部分的文章會講到組合是聚合(aggregation)的一種,而它們都是源自於UML的產物,實際上UML定義的定義很模糊也很難理解。因此,我要講的是它們最基本的一面,也就是 Is-A
和 Has-A
關係:
很多文章和書都建議我們要多用組合少用繼承,這是因為繼承會對物件造成巨大的依賴關係。我們用一個範例來說明組合:
// 定義一個英雄結構,包含了正常人結構
type Hero struct {
Person *Person
HeroName string
HerkRank int
}
// 定義一個正常人結構
type Person struct {
Name string
}
func main() {
var tony = &Hero{&Person{"Tony Stark"}, "Iron Man", 1}
fmt.Printf("Hero=%+v\n", *tony)
fmt.Printf("Person=%+v\n", *(tony.Person))
}
執行結果:
Hero={Person:0xc0000841e0 HeroName:Iron Man HerkRank:1}
Person={Name:Tony Stark}
上面範例中,我們看到了所謂的組合就是結構再包結構的概念,透過這樣的方式共享結構資料或方法。
再來談談go語言的內嵌特性,這個特性並沒有寫在A Tour of Go,而是在Effective Go裡頭。
Effective Go: Embedding
Go語言的內嵌其實就是組合的概念,只是它更加簡潔及強大。內嵌允許我們在結構內組合其他結構時,不需要定義欄位名稱,並且能直接透過該結構叫用欄位或方法。我們將上面的範例改成使用內嵌,如下:
// 定義一個英雄結構
type Hero struct {
*Person // 不需要欄位名稱
HeroName string
HerkRank int
}
// 定義一個正常人結構
type Person struct {
Name string
}
func main() {
var tony = &Hero{
&Person{"Tony Stark"},
"Iron Man",
1}
fmt.Printf("%s\n", tony.Name) // 直接叫用內部結構資料
// 等於 fmt.Printf("%s\n", tony.Person.Name)
}
// 執行結果: Tony Stark
實際上,內嵌的結構欄位還是會有名稱,就是和結構本身的名稱同名。
另外,上面範例是用匿名初始化,也可以使用具名初始化,差別在於初始化參數的數量和順序是可以被調整的:
var tony = &Hero{
Person: &Person{"Tony Stark"},
HeroName: "Iron Man",
HeroRank: 1}
上面看到的範例都是內嵌結構資料,現在我們來試試看內嵌結構方法,修改同一個範例如下:
// 定義一個英雄結構
type Hero struct {
*Person
HeroName string
HeroRank int
}
// 英雄都會飛
func (*Hero) Fly() {
fmt.Println("I can fly.")
}
// 定義一個正常人結構
type Person struct {
Name string
}
// 正常人會走路
func (*Person) Walk() {
fmt.Println("I can walk.")
}
func main() {
var tony = &Hero{
Person: &Person{"Tony Stark"},
HeroName: "Iron Man",
HeroRank: 1}
tony.Walk() // 等於 tony.Person.Walk()
tony.Fly()
}
執行結果:
I can walk.
I can fly.
當有多個內嵌結構時,就有可能發生欄位同名的問題。我們稍微修改一下範例,超級英雄也會想養一隻寵物,這很合理的。因此,我們就加入一個寵物結構:
// 定義一個英雄結構
type Hero struct {
*Person
*Pet
HeroName string
HeroRank int
}
// 定義一個正常人結構
type Person struct {
Name string
}
// 定義一個寵物結構
type Pet struct {
Name string
}
func main() {
var tony = &Hero{
Person: &Person{"Tony Stark"},
Pet: &Pet{"Pepper"},
HeroName: "Iron Man",
HeroRank: 1}
fmt.Printf("%s\n", tony.Name)
}
由於 Person 和 Parner 都有 Name 這個欄位,直接叫用 tony.Name 就會產生衝突,編譯器會顯示錯誤訊息:
./main.go:40:25: ambiguous selector tony.Name
事實上,可以被內嵌的型別不只有結構,也可以是基本型別,範例如下:
type Data struct {
int
string
float32
bool
}
func main() {
var data = &Data{1, "Iron Man", 1.2, true}
fmt.Println(*data)
fmt.Printf("%+v \n", *data)
}
執行結果
{1 Iron Man 1.2 true}
{int:1 string:Iron Man float32:1.2 bool:true}
基本型別被內嵌之後,欄位名稱就是型別的原始名稱,ex: int, string, ...。
今天介紹了go語言的內嵌特性,使得沒有繼承的go語言,依然可以相互共享結構內的程式碼。而這樣的作法在實務上究竟是否優於繼承,可能需要寫久一點,才會深刻了解。