# Day 36 : Golang - Go的物件導向設計 ## 前言 在說明完Struct和Interface,以及Python和JaavaScript如何實現物件導向,這次用Go的角度看物件導向。 [官方文件](https://go.dev/doc/faq#Is_Go_an_object-oriented_language): > Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. 其答案非常模糊,自己也沒辦法斷定,但是不是也可思考成答案不是這麼重要? Go沒有class存在,沒有class階層,沒有物件的概念,一般來說不會說是物件導向的程式語言,但確實是可以用不同方法來實現物件導向。那接下來就看看Go可以如何展現OOP的特性吧。 ## Struct is similar to Class 在Go如果有一組資料總是常常會需要到,我們會使用struct來自行定義,而如果需要一個函式專門用來處裡某個struct,可以定義一個receiver function。 就像我們定義一個Pet型別的sturct,透過指定reciver為一個Pet類別的變數p,建立一個myInfo()的函式,當作Pet的方法。這時候在第21行使用`p.myInfo()`的動作稱為"selector",即它為Pet型別的receiver p選擇一個適當的方法。 ```go= package main import ( "fmt" ) type Pet struct { petType string breed string } func (p Pet) myInfo() string{ return fmt.Sprintf("I am a %s and my breed is %s",p.petType,p.breed) } func main() { p :=Pet{ petType:"dog", breed:"Golden Retriever", } fmt.Println(p.myInfo()) } ``` ## 繼承 在Go裡面透過composition來達成繼承的效果,composition就是透過將一個或多個物件放到另一個物件裡面,以Go來看就是建立一個struct型別再放入另一個struct 型別,此方法又稱為型別內嵌(type embedding)。 例如我新增一個Dog的結構體然後內嵌Pet結構體,其名稱和型別都是Pet。 然後於第17行初始化空的結構體,再透過`.`增加欄位的值(當然也可以初始化的時候就直接加進去),可以看到dog也可以使用Pet的屬性和方法了。 ```go= type Pet struct { petType string breed string } type Dog struct{ Pet age int name string } func (p Pet) myInfo() string{ return fmt.Sprintf("I am a %s and my breed is %s",p.petType,p.breed) } func main() { dog:=Dog{} dog.age=7 dog.name="Brown" dog.Pet = Pet{ petType: "dog", breed: "Golden Retriever", } fmt.Println(dog.myInfo()) // I am a dog and my breed is Golden Retriever fmt.Println(dog.name) // Brown fmt.Println(dog.breed) // Golden Retriever } ``` ## 封裝 一個變數的屬性或是方法無法被使用就是被封裝(encapsulated),在Go只有一個機制,就是透過變數第一個字是否大寫來判斷,如為大寫可以被其他package使用,小寫則不行。 這邊使用在[Day 22 : Golang Struct(2)](https://hackmd.io/gLY_tf7MR5ycUDy_Spqn2A)說明的例子 當User結構體有三個fields:id,lastname,contact,其中contact是一個內嵌的結構體。print()函式為一個屬於User結構使用的方法,可以印出該型別變數的資訊,並建立兩個檔案,user/user.go、main.go。 在user.go定義了公開的User結構和私有的field和公共的方法Print()、UpdateName()。 ```go! // user/user.go package user import "fmt" type User struct { id int lastname string contact ContactInfo } type ContactInfo struct { email string zipCode int } func NewUser(id int, lastname string, email string, zipCode int) *User { contact := ContactInfo{email, zipCode} return &User{id, lastname, contact} } func (u *User) Print() { fmt.Printf("%v\n", u) } func (u *User) UpdateName(lastname string) { u.lastname = lastname } ``` 在main.go裡面先透過NewUser()函式建立一個新的User物件,並取得該物件的指標。之後就可以透過指標使用物件的方法,但在第11行英文`lastaname`是私有的,所以無法存取會發生錯誤,而Print()和UpdateName()都是公開的方法所以可已執行。 ```go= //main.go package main import ( "playground/user" ) func main() { dylan:= user.NewUser(123,"Huang","dylan123@gmail.com",456) // fmt.Println(dylan.lastname) dylan.Print() // &{123 Huang {dylan123@gmail.com 456}} dylan.UpdateName("Wu") dylan.Print() // &{123 Wu {dylan123@gmail.com 456}} } ``` ## 多型 當不同變數呼叫同一個方法可以得到不同結果就是多型,最簡單的例子如下,透過實體化兩個不同的狗狗,呼叫myInfo()會得到不同的結果。 ```go= type Pet struct { petType string breed string } type Dog struct{ Pet age int name string } func (p Pet) myInfo() string{ return fmt.Sprintf("I am a %s and my breed is %s",p.petType,p.breed) } func main() { dog1 :=Dog{ Pet: Pet{ petType:"dog", breed:"Golden Retriever", }, age:7, name:"Brown", } dog2 :=Dog{ Pet: Pet{ petType:"dog", breed:"Samoyed", }, age:1, name:"White", } fmt.Println(dog2.myInfo()) // I am a dog and my breed is Samoyed fmt.Println(dog1.myInfo()) // I am a dog and my breed is Golden Retriever } ``` 但這樣不就太普通了? 記得之前花很多時間學習的interface嗎,在Go裡面多型就是透過interface來執行,最經典的例子就是io package和fmt package,想想為什麼`fmt.Println()`可以接受任何型別的變數呢,因為在此函數裡面參數的型別是一個空介面,任何變數都會實現空介面,所以都可以正常輸出,這就是一個多型的概念,在此當然也要修改一下狗狗這個例子。 先定義一個Pet介面,裡面有myInfo()這個方法。 接著定義一個Dog結構體,且該Dog型別也有個專屬的函式myInfo(),所以可以說明Dog型別實現了Pet介面,所以它可以被賦值给Pet接口型別的變數 pet1,這時候pet1就可以使用Dog結構體中實現的myInfo()。 ```go! package main import ( "fmt" ) type Pet interface { myInfo() string } type Dog struct{ petType string breed string age int name string } func (d Dog) myInfo() string{ return fmt.Sprintf("I am a %s and my breed is %s", d.petType, d.breed) } func main() { dog1 := Dog{ petType:"dog", breed:"Golden Retriever", age:7, name:"Brown", } dog2 := Dog{ petType:"dog", breed:"Samoyed", age:1, name:"White", } var pet1 Pet pet1 = dog1 fmt.Println(pet1.myInfo()) // I am a dog and my breed is Golden Retriever var pet2 Pet pet2 = dog2 fmt.Println(pet2.myInfo()) // I am a dog and my breed is Samoyed } ``` ## References 1. [從Go語言看物件導向](https://www.ithome.com.tw/voice/107839) 2. [Introduction to Object-oriented Programming in Go](https://www.developer.com/languages/oop-go/) 3. [Go是一门面向对象编程语言吗](https://tonybai.com/2023/03/12/is-go-object-oriented/) ###### tags: `About Go`