兩分鐘讓你明白Go中如何繼承

最近在重構代碼的時候,抽象了大量的接口。也使用這些抽象的接口作了不少僞繼承的操做,極大的減小了代碼冗餘,同時也增長了代碼的可讀性。java

而後隨便搜了一下關於Go繼承的文章,發現有的文章的代碼量過多,而且代碼format極其粗糙,命名極其隨意,相似於A、B這種,讓人看着看着就忘了究竟是誰繼承誰,我又要回去看一遍邏輯。web

雖然只是樣例代碼,我認爲仍然須要作到簡潔、清晰以及明瞭。這也是我爲何要寫這篇博客的緣由。接下里在這裏簡單分享一下在Go中如何實現繼承。服務器

1. 簡單的組合

說到繼承咱們都知道,在Go中沒有extends關鍵字,也就意味着Go並無原生級別的繼承支持。這也是爲何我在文章開頭用了僞繼承這個詞。本質上,Go使用interface實現的功能叫組合,Go是使用組合來實現的繼承,說的更精確一點,是使用組合來代替的繼承,舉個很簡單的例子。微信

1.1 實現父類

咱們用很容易理解的動物-來舉例子,廢話很少說,直接看代碼。app

type Animal struct {
	Name string
}

func (a *Animal) Eat() {
	fmt.Printf("%v is eating", a.Name)
	fmt.Println()
}

type Cat struct {
	*Animal
}

cat := &Cat{
	Animal: &Animal{
		Name: "cat",
	},
}
cat.Eat() // cat is eating
複製代碼

1.2 代碼分析

首先,咱們實現了一個Animal的結構體,表明動物類。並聲明瞭Name字段,用於描述動物的名字。frontend

而後,實現了一個以Animal爲receiver的Eat方法,來描述動物進食的行爲。函數

最後,聲明瞭一個Cat結構體,組合了Cat字段。再實例化一個貓,調用Eat方法,能夠看到會正常的輸出。微服務

能夠看到,Cat結構體自己沒有Name字段,也沒有去實現Eat方法。惟一有的就是組合了Animal父類,至此,咱們就證實了已經經過組合實現了繼承。性能

2. 優雅的組合

熟悉Go的人看到上面的代碼可能會發出以下感嘆學習

這也太粗糙了吧 -- By 魯迅:我沒說過這句話

的確,上面的僅僅是爲了給尚未了解過Go組合的人看的。做爲一個簡單的例子來理解Go的組合繼承,這是徹底沒有問題的 。但若是要運用在真正的開發中,那仍是遠遠不夠的。

舉個例子,我若是是這個抽象類的使用者,我拿到animal類不能一目瞭然的知道這個類幹了什麼,有哪些方法能夠調用。以及,沒有統一的初始化方式,這意味着凡是涉及到初始化的地方都會有重複代碼。若是後期有初始化相關的修改,那麼只有一個一個挨着改。因此接下來,咱們對上述的代碼作一些優化。

2.1 抽象接口

接口用於描述某個類的行爲。例如,咱們即將要抽象的動物接口就會描述做爲一個動物,具備哪些行爲。常識告訴咱們,動物能夠進食(Eat),能夠發出聲音(bark),能夠移動(move)等等。這裏有一個頗有意思的類比。

接口就像是一個招牌,好比一家星巴克。星巴克就是一個招牌(接口)。

你看到這個招牌會想到什麼?美式?星冰樂?抹茶拿鐵?又或者是拿鐵,甚至是店內的裝修風格。

這就是一個好的接口應該達到的效果,一樣這也是爲何咱們須要抽象接口。

// 模擬動物行爲的接口
type IAnimal interface {
	Eat() // 描述吃的行爲
}

// 動物 全部動物的父類
type Animal struct {
	Name string
}

// 動物去實現IAnimal中描述的吃的接口
func (a *Animal) Eat() {
	fmt.Printf("%v is eating\n", a.Name)
}

// 動物的構造函數
func newAnimal(name string) *Animal {
	return &Animal{
		Name: name,
	}
}

// 貓的結構體 組合了animal
type Cat struct {
	*Animal
}

// 實現貓的構造函數 初始化animal結構體
func newCat(name string) *Cat {
	return &Cat{
		Animal: newAnimal(name),
	}
}

cat := newCat("cat")
cat.Eat() // cat is eating
複製代碼

在Go中其實沒有關於構造函數的定義。例如咱們在Java中可使用構造函數來初始化變量,舉個很簡單的例子,Integer num = new Integer(1)。而在Go中就須要使用者本身經過結構體的初始化來模擬構造函數的實現。

而後在這裏咱們實現子類Cat,使用組合的方式代替繼承,來調用Animal中的方法。運行以後咱們能夠看到,Cat結構體中並無Name字段,也沒有實現Eat方法,可是仍然能夠正常運行。這證實咱們已經經過組合的方式了實現了繼承。

2.2 重載方法

// 貓結構體IAnimal的Eat方法
func (cat *Cat) Eat() {
	fmt.Printf("children %v is eating\n", cat.Name)
}

cat.Eat()
// children cat is eating
複製代碼

能夠看到,Cat結構體已經重載了Animal中的Eat方法,這樣就實現了重載。

2.3 參數多態

什麼意思呢?舉個例子,咱們要如何在Java中解決函數的參數多態問題?熟悉Java的可能會想到一種解決方案,那就是通配符。用一句話歸納,使用了通配符可使該函數接收某個類的全部父類型或者某個類的全部子類型。可是我我的認爲對於不熟悉Java的人來講,可讀性不是特別友好。

而在Go中,就十分方便了。

func check(animal IAnimal) {
	animal.Eat()
}
複製代碼

在這個函數中就能夠處理全部組合了Animal的單位類型,對應到Java中就是上界通配符,即一個能夠處理任何特定類型以及是該特定類型的派生類的通配符,再換句人話,啥動物都能處理。

3. 總結

凡事都有兩面性,作優化也不例外。大量的抽象接口的確能夠精簡代碼,讓代碼看起來十分優雅、舒服。可是一樣,這會給其餘不熟悉的人review代碼形成理解成本。想象你看某段代碼,全是接口,點了好幾層才能看到實現。更有的,往下找着找着忽然就在另外一個接口處斷掉了,必需要手動的去另外一個註冊的地方去找。

這就是我認爲優化的時候要面臨的幾個問題:

  • 優雅
  • 可讀
  • 性能

有的時候咱們很難作到三個方面都兼顧,例如這樣寫代碼看起來很難受,可是性能要比優雅的代碼好。再例如,這樣寫看起來很優雅,可是可讀性不好等等。

仍是引用我以前博客中常常寫的一句話

適合本身的纔是最好的

這種時候只能根據本身項目的特定狀況,選擇最適合你的解決方案。沒有萬能的解決方案。

分享一句最近彈吉他看到的毒雞湯,學習也是同樣的。

練琴的路上沒有捷徑,全是彎路

往期文章:

相關:

  • 微信公衆號: SH的全棧筆記(或直接在添加公衆號界面搜索微信號LunhaoHu)
相關文章
相關標籤/搜索