最後更新於 2021 年 5 月 23 日
在不同程式語言有不同的OOP實現方式,例如JAVA支持extends也支持interface,但是Go並沒有像C或者JAVA的Class類,它只有C語言的結構體,用結構體和指針等特性完成一個類的功能。
物件導向的基本概念
基本概念必須先知道 類別
、物件
究竟是什麼?
類別(Class)
類別定義了一個事物的抽象特點,類的定義包含了資料的形式以及對資料的操作。
這樣講有點過於抽象,我們可以直接透過下面程式碼來理解:
首先,我們定義一個類別為 汽車,這個類別會包含車的相關屬性,比如 汽車廠牌、汽車型號、汽車重量,而汽車可以發動、熄火、打開油箱,我們將這些行為定義為該類別的方法。
type 汽車 struct { 廠牌 string //屬性 型號 string //屬性 重量 int //屬性 } func 發動(car 汽車) { //方法 //... } func 熄火(car 汽車) { //方法 //... } func 打開油箱(car 汽車) { //方法 //... }
這段程式碼代表我聲明了一個類別 汽車,小說這個類別具備了汽車的特徵以及一些動作。
物件(Object)
物件是類別的實例,在定義了汽車類別之後,我們可以用這個類別來定義物件:
小白 := 汽車{"Toyota", "RAV4", 1610} 小黑 := 汽車{"Porsche", "718 Cayman GT4", 1365}
我們可以讓小白小黑發動 (同理 熄火、打開油箱)
小白.發動() 小黑.發動()
有了類別和物件的概念之後接著就要了解物件導向的三種特性。
物件導向的三大特性
物件導向的三大特性:封裝、繼承、多形
封裝
其實在剛剛物件的地方我們就已經接觸過封裝了。封裝即是將物件裡的資料隱藏起來,只需透過物件本身所提供的介面去取得物件內部屬性或方法,其他物件無法了解此物件的內部細節,我們可以透過 發動()
來使小白這台汽車發動,但是我們並不需要知道他是如何去發動的。
小白.發動() //發動() 定義了汽車具體該通過什麼方法發動,但是小白並不知道它到底是如何發動的。 小黑.發動()
繼承
在某些情況下,一個類別會有它的子類別,例如汽車這個子類別可能會有「跑車」「休旅車」..等,這種情況下「小白」可能就是休旅車的一個實例,而「小黑」則可能為跑車的一個實例。
子類別會繼承父類的屬性和行為,並且也可以加入子類自己的方法。
因為 Go語言中該如何去繼承還沒有講到,所以我使用虛擬碼來舉例:
類別 跑車 繼承 汽車 開始 掀開車頂() 結束
跑車因為繼承了汽車,所以跑車同時也有父類汽車的 廠牌、型號、重量 三個屬性,以及 發動( )、熄火( )、打開油箱( ) 三個方法,同時還有跑車自己的方法 掀開車頂( ) 。
多形
多形是指由繼承而產生的相關的不同的類別,其物件對同一方法會做出不同的動作。
舉例來說,跑車和休旅車都有 掀開敞篷() 這一方法,但是跑車的 掀開敞篷() 需要三步驟 而 休旅車的 掀開敞篷() 一步到位 *此處僅為舉例,並不代表現實如此。
類別 跑車 開始 掀開車頂() 開始 三步驟() 結束 結束 類別 休旅車 開始 掀開車頂() 開始 一步驟() 結束 結束
Go語言中實現封裝、繼承、多形
封裝
在其他語言的類屬性和實例函數,Go以struct
和func
實現。
實作一個Hero類,有三個屬性分別為:name、ad、level,並且擁有三個個實例函數 Show() GetName() SetName(),要注意記得在 func
關鍵字與函數名稱之間加上一個 ()
在其中告知函數綁定哪個結構體
package main import "fmt" type Hero struct { name string ad int level int } func (my Hero) Show() { //當前方法屬於哪個結構體() fmt.Println("Name = ", my.name) fmt.Println("Ad = ", my.ad) fmt.Println("Level = ", my.level) } func (my Hero) GetName() { fmt.Println("Name = ", my.name) } func (my Hero) SetName(newName string) { //my 是調用該方法的物件的一個副本 my.name = newName } func main() { Hero := Hero{name: "小明", ad: 100, level: 10} Hero.Show() } /* Name = 小明 Ad = 100 Level = 10 */
如果有嘗試去使用GetName或SetName兩個方法的話,會發現這兩個方法並沒有任何作用。那麼我們應該如何做才會使GetName和SetName有效呢?
當然就是指針啦!
將方法改為:
type Hero struct { name string ad int Level int } func (my *Hero) Show() { fmt.Println("Name = ", my.name) fmt.Println("Ad = ", my.ad) fmt.Println("Level = ", my.Level) } func (my *Hero) GetName() { fmt.Println("Name = ", my.name) } func (my *Hero) SetName(newName string) { my.name = newName } func main() { Hero := Hero{name: "小明", ad: 100, Level: 10} Hero.Show() Hero.SetName("老王") Hero.Show() } /* Name = 小明 Ad = 100 Level = 10 Name = 老王 Ad = 100 Level = 10 */
繼承
父類
定義
使用 type
和 struct
來定義一個類別
type Human struct { name string sex string }
方法的實現
我們可以透過在 func
關鍵字與方法名稱之間加上 (arg 父類)
來定義類別方法:
func (arg Human) Eat() { fmt.Println("Human.Eat()...") }
這裡的 arg
僅是調用該方法的物件的一個副本,取什麼都行。
子類
繼承父類
在子類中直接輸入父類的名稱即可引入父類的所有屬性和方法,並且我們可以重定義父類方法和加入子類的新方法:
type superMan struct { Human //superMan 繼承了Human類的方法 level int } //重定義父類方法 func (my superMan) Eat(){ fmt.Println("superMan.Eat()...") } //子類的新方法 func (my superMan) Fly(){ fmt.Println("superMan.Fly()...") }
定義子類
- 宣告時直接初始化值
s := superMan{ Human{"小明","男"}, 100}
- 先宣告再賦值
var s superMan s.name = "小王" s.sex = "男" s.level = 55
多形
在Go語言中我們透過實作 interface 來實現多形,interface本質是一個指針,多形就是父類指針指向子類。
多形基本要素
1.有一個父類(有interface)
2.有子類(實現了父類的全部interface)
3.父類型別的變數(指針)指向(引用)子類的具體變數
使用 type name interface
定義一個 AnimalIF 介面:
//定義一個interface type AnimalIF interface { Sleep() GetColor() string //獲取動物的顏色 GetType() string //獲取動物的種類 }
再使用 struct
定義 Cat 和 Dog 類,這兩個類底下還有Sleep( )、GetColor( )、GetType( )三個方法:
//具體的類 type Cat struct { color string //貓的顏色 } //重寫接口方法 func (my *Cat) Sleep() { fmt.Println("Cat is sleep") } func (my *Cat) GetColor() string { return my.color } func (my *Cat) GetType() string { return "Cat" } //具體的類 type Dog struct { color string } //重寫接口方法 func (my *Dog) Sleep() { fmt.Println("Dog is sleep") } func (my *Dog) GetColor() string { return my.color } func (my *Dog) GetType() string { return "Dog" }
然後我們可以通過宣告一個interface型別的變數,將 AnimalIF 指向 Cat 和 Dog,便能透過接口實現具體的類:
func main() { var animal AnimalIF //interface型別 , 父類指針 animal = &Cat{"Brown"} animal.Sleep() // 調用的就是 Cat 的 Sleep() 方法 , Cat is sleep animal = &Dog{"Yellow"} animal.Sleep() //Dog is sleep }
透過 interface 來實現多形
func showAnimal(animal AnimalIF) { animal.Sleep() //多形 fmt.Println("color = ",animal.GetColor()) fmt.Println("kind = ",animal.GetType()) } func main() { cat := Cat{"Brown"} dog := Dog{"Yellow"} showAnimal(&cat) showAnimal(&dog) }
通用萬能型別
可以用空介面 interface { }
型別引用任意的資料型別。
舉例來說:
func myFunc(arg interface{}) { fmt.Println("myFunc is called...") fmt.Println(arg) } type Book struct { auth string } func main() { book := Book{"Golang"} myFunc(book) myFunc(100) myFunc("abc") myFunc(3.14) } /* myFunc is called... {Golang} myFunc is called... 100 myFunc is called... abc myFunc is called... 3.14 */
由此可知空介面為萬能型別
型別斷言
那麼空介面該如何區分此時引用的資料型別到底是什麼?
func myFunc( arg interface{} ) { fmt.Println("myFunc is called...") fmt.Println(arg) //給 interface{} 提供 "型別斷言" 的機制 value,ok := arg.(string) //必須得是空介面才行 , 返回value,ok if !ok { fmt.Println("arg is not string type") }else{ fmt.Println("arg is string type , value = ",value) fmt.Printf("value type is %T\n",value) } }
完整例子:
func myFunc(arg interface{}) { fmt.Println("myFunc is called...") fmt.Println(arg) value,ok := arg.(string) //必須得是空接口才行 , 返回value,ok if !ok { fmt.Println("arg is not string type") }else{ fmt.Println("arg is string type , value = ",value) fmt.Printf("value type is %T\n",value) } } type Book struct { auth string } func main() { book := Book{"Golang"} myFunc(book) myFunc(100) myFunc("abc") myFunc(3.14) } /* myFunc is called... {Golang} arg is not string type myFunc is called... 100 arg is not string type myFunc is called... abc arg is string type , value = abc value type is string myFunc is called... 3.14 arg is not string type */
- React 那些好看、有趣、實用的函式庫、組件庫推薦(2) - 2022 年 6 月 26 日
- 解決 preact-router 資源請求路徑錯誤的問題 - 2022 年 6 月 24 日
- [楓之谷私服] 潮流轉蛋機 NPC 腳本優化 - 2022 年 6 月 19 日