let's GoLang(二): 面向物件
這是GoLang系列文章的第二篇:面向物件。上一篇是Let's GoLang(一): 反射 。
嚴格來說,說到面向物件,那麼一定會談到Java,Go並沒有Java中那種面向物件的概念,但是跟JS基於原型的面向物件一樣,可以通過組合Go的一些特性來實現面向物件的功能。
說到面向物件,那麼一定少不了繼承、封裝、多型 。
封裝
首先我們來了解一下Go中的封裝。對於封裝而言,一個物件它所封裝的是自己的屬性和方法 ,所以它是不需要依賴其他物件就可以完成自己的操作。使用封裝的好處是:良好的封裝能夠減少耦合,類內部的結構可以自由修改;可以對成員進行更精確的控制。隱藏資訊,實現細節 。在Go中,封裝是基於可見性原則來實現的,我們來看個例子:
package oop type User struct { Name string age int } func (user *User) SetAge(age int) *User{ user.age = age return user } func (user *User) GetAge () int{ return user.age }
package main import ( "fmt" "github.com/anymost/oop" ) func main(){ user := oop.User{ Name: "jack", } user.SetAge(20) fmt.Println(
在Golang中,基於可見性原則,如果一個變數是以大寫字母開頭的,那麼在包外面可以訪問到;如果是以小寫字母開頭的,在包內可見,在包外不可見 。
我們可以看到上面的例子,在一個oop的包內定義了一個User的結構體,那麼我們能在包外訪問到這個結構體。User結構體中,Name欄位是以大寫字母開頭的,可以直接在包外訪問;age欄位是以小寫欄位,在包外不可見,但是我們可以提供了以大寫字母開頭的GetAge和SetAge方法,同樣能夠讀取和修改age欄位。這就是Go裡面的封裝,很好理解。
繼承
通常來說,繼承是使用已存在的類的定義作為基礎建立新類的技術 ,新類的定義可以增加新的資料或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。通過使用繼承我們能夠非常方便地複用以前的程式碼,能夠大大的提高開發的效率。在Java中,子類繼承父類,那麼能夠通過子類來訪問到父類上的屬性和方法。
在Go中,繼承是基於組合的方式來實現的,我們來看個例子:
package oop type Car struct { Engine string Tire string } type SUV struct { Car Brand string }
package main import ( "fmt" "github.com/anymost/oop" ) func main(){ suv := oop.SUV{Car: oop.Car{Engine: "good", Tire: "fine"}, Brand: "BMW"} fmt.Println(suv.Engine) fmt.Println(suv.Brand) }
可以從上面的程式碼看到,我們首先定義了一個Car的struct,然後定義了一個SUV的struct,其中SUV包含Car,能夠通過SUV來訪問到Car中的屬性和方法等,這就是struct的組合。不僅能夠組合struct,還能組合interface:
package oop type Dancer interface { Dance() } type Singer interface { Sing() } type Artist interface { Dancer Singer }
上面分別定義了Dancer,Singer,Artist三個interface,如果要滿足Artist型別,就需要定義Dance和Sing兩個方法
多型
接下來,我們理解一下多型。 在Java中基於繼承的實現機制主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行為 。基於繼承實現的多型可以總結如下:對於引用子類的父類型別,在處理該引用時,它適用於繼承該父類的所有子類,子類物件的不同,對方法的實現也就不同,執行相同動作產生的行為也就不同。如果父類是抽象類,那麼子類必須要實現父類中所有的抽象方法,這樣該父類所有的子類一定存在統一的對外介面,但其內部的具體實現可以各異。這樣我們就可以使用頂層類提供的統一介面來處理該層次的方方法。
在Go中,多型是基於interface來實現的,我們還是舉一個簡單的例子:
package oop import "fmt" type Animal interface { Live() } type Cat struct { Name string } type Dog struct { Name string } func (cat *Cat) Live(){ fmt.Println(fmt.Sprintf("%s eat fish", cat.Name)) } func (dog *Dog) Live(){ fmt.Println(fmt.Sprintf("%s eat bone", dog.Name)) }
package main import "github.com/anymost/oop" func showLive(animals []oop.Animal){ for _, animal := range animals { animal.Live() } } func main(){ animals := []oop.Animal{ &oop.Cat{Name: "cat"}, &oop.Dog{Name:"dog"}, } showLive(animals) }
我們來分析上面的程式碼:
- 首先定義了一個名為Animal的介面,其中包含了Live方法
- 然後分別定義了名為Cat、Dog 的兩個struct,在它們上定義了Livefangfa
- 然後定義了名為showLive的函式,引數為Animal型別的陣列
- 最後定義了一個animals的陣列,其成員分別為Cat,Dog的例項
總結一下:在Go中,只要在一個struct上定義了某個interface的所有方法,那麼這個struct就是可以認為是這個interface的型別,可以在所有使用到這個interface型別的地方使用該struct,就像上面的showLive方法一樣。同時,Cat,Dog雖然是Animal型別的,但是又擁有自己的邏輯,這就是多型的強大。那麼問題來了,我們怎麼來判斷一個interface的變數具體是哪個型別的呢?這就要用到我們前面提到的反射了:
package main import ( "fmt" "github.com/anymost/oop" ) func checkType(animals []oop.Animal){ for _, animal := range animals { switch animal.(type) { case *oop.Cat: fmt.Println("type is Cat") case *oop.Dog: fmt.Println("type is Dog") default: fmt.Println("type is others") } } } func main(){ animals := []oop.Animal{ &oop.Cat{Name: "cat"}, &oop.Dog{Name:"dog"}, } checkType(animals) }
我們可以看到,藉助反射,能夠在執行時來動態的判斷型別。而且,Go中還有類似於Java中的Object型別,那就是空介面 interface{},所有型別的變數都可以認為是interface{}型別的。