Go 並不是以類別為基礎的物件導向設計,而是以 Struct 及 Method 為基礎的設計,這使 Go 非常適合處理網路和並行程式設計,並能快速地編譯和執行。
儘管 Go 沒有傳統的類別繼承和多型的概念,但是它仍然有封裝、繼承和多型的特性。例如,在 Go 中,可以透過結構 struct
和方法 method
的搭配實現封裝的概念。而多型則可以透過接口 interface
來實現。雖然 Go 的物件導向設計與其他語言有所不同,但是它仍然可以實現物件導向的程式設計。
在 Go 中,方法(Method)是一個好用的東西,它可以封裝一個功能,並且可以重複使用,可以讓我們寫出更簡潔的程式碼。
它大致長這樣:
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
先定義 Rectangle
的 Struct
,接著,定義一個 Area()
的 Method
,並在裡面寫計算面積的公式,這麼一來其他的函式需要計算長方形面積就可以使用這個 Method
。
題外話:
Ruby 有關 Method 的介紹可以參考這篇文章:Ruby 使用一些方法吧
舉一個例子,讓我們去比較不使用 Method 與使用 Method 的差別,所謂的沒有比較沒有傷害。
先來定義一個 Struct
叫做長方形 ( Rectangle ),現在要計算它的面積,一般的思路會是下方這樣:
package main
import "fmt"
type Rectangle struct{
width, height float64
}
func area(r Rectangle) float64{
return r.width * r.height
}
func main(){
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1:", area(r1))
fmt.Println("Area of r2:", area(r2))
}
執行結果
Area of r1: 24
Area of r2: 36
上面的程式碼可以計算出長方形的面積,但要注意一點,area()
不是作為 Rectangle
的方法 (類似物件導向裡面的方法) 去實現的,而是將 Rectangle
的物件作為參數傳入 area()
去計算出面積的。
這樣做當然沒什麼問題,但是未來要增加圓型、正方形、三角形甚至其他多邊型,當要計算它們的面積,那以上面的方法,你就只能增加新的函式,函式名也必須更換,變成 area_rectangle
, area_circle
, area_triangle
...
上圖為 Method
與 Struct
的關係圖。
可以發現函式並不屬於 Struct
,反倒是單獨存在於 Struct
外,而非概念上屬於某個 Struct
。
以上的概念,用物件導向的術語來說,就是不屬於某個 class
。
從上面的例子來看,這樣的寫法並不是很優雅,並且從概念來說面積是形狀的一種屬性,它是屬於這個特定形狀的,就像長方形的長和高一樣。
這時就可以使用 Method
,Method
是附屬在一個給定的型別 (type) 上,它的語法與函式宣告的語法幾乎一樣,只是多了一個 receiver
也就是 Method
所依從的主體。
Method
的語法長這樣:
func (r ReceiverType) funcName(paramters) (results){
// code
}
func
: 宣告一個方法(r ReceiverType)
: 表示宣告一個接收器(receiver),接收器是一個類型 (Type) 可以是一個指標 (Pointer) 或是一個值 (Value),它位於 func 關鍵字和方法名稱之間,用來綁定方法的作用對象。ReceiverType 就是方法所屬的類型 (Type)。funcName
: 是這個 Method 的名字,用來識別這個 Method,通常使用駝峰式命名法。parameters
: 是這個 Method 的參數列表,可以是任意數量的參數。results
: 是方法的返回值,和普通函式一樣,可以有多個返回值,如果沒有返回值,則可以省略。注意!!!!!
在使用 Method 時要注意以下幾點:
Method
的名字一模一樣,但是接收者不一樣,那麼 Method
就會不一樣。Method
裏可以存取接收者的欄位。Method
是透過 .
存取,就像 Struct
存取欄位一樣。我們將最一開始的例子用 Method
改寫:
package main
import (
"fmt"
"math"
)
type Rectangle struct{
width, height float64
}
type Circle struct{
radius float64
}
func (r Rectangle) area() float64{
return r.width * r.height
}
func (c Cricle) area() float64{
return c.radius * math.Pi
}
func main(){
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{69}
fmt.Println("Area of r1: ", r1.area())
fmt.Println("Area of r2: ", r2.area())
fmt.Println("Area of c1: ", c1.area())
fmt.Println("Area of c2: ", c2.area())
}
執行結果
Area of r1: 24
Area of r2: 36
Area of c1: 18.84955592153876
Area of c2: 72.25663103256524
從上面的例子來看,方法 Method
area()
是依賴某個形狀來發生作用的,像是 Circle
,Circle.area()
的發出者是 Circle
,而 area()
是屬於 Circle
的方法,而不是外面的函式。
具體說,Circle 存在欄位 radius
,同時存在方法 area()
, 這些欄位和方法都屬於 Circle
。而 Rectangle
存在欄位 height
和 width
, 也同時存在方法 area()
, 這些欄位和方法都屬於 Rectangle
。
把上面的概念畫成圖會比較清楚:Method
area()
分別屬於 Rectangle
和 Circle
, 於是他們的 Receiver
就變成了 Rectangle
和 Circle
, 或者說,這個 area()
方法 是由 Rectangle
/Circle
發出的。
從一開始到到現在,只看到 Method 只作用於 Struct 上,但其實它還可以定義在任何你自訂的型別、內建型別、Struct等各種型別上。
等等!什麼事自訂的行別,不就是 Struct 嗎?其實不然,Sturct 只是自訂型別中比較特殊的型別,還有其他自訂型別宣告,可以透過以下的宣告來實現。
自訂型別長得像以下這樣:
type typeName typeLiteral
typeLiteral : 是該新資料型別所基於的現有資料型別。typeLiteral
可以是任何一種現有資料型別,例如 int
、float
、string
、bool
等等,也可以是結構體、介面、陣列等自定義型別。
舉例來說:
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...// 省略
"December":31,
}
是不是很簡單!這樣就可以在程式碼中定義有意義的型別了。
既然知道自訂型別是什麼了!那就可以來定義超多複雜的 Method 了。
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (bl BoxList) BiggestColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if bv := b.Volume(); bv > v {
v = bv
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack() {
for i := range bl {
bl[i].SetColor(BLACK)
}
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (c Color) String() string {
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main() {
boxes := BoxList {
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestColor().String())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}
看到上面東西是不是頭昏眼花了呢?馬上解釋上面的程式碼是在做甚麼。
首先,先自訂一些型別:
接著以上面自訂型別為接收者定義一些 method:
上面的程式碼透過文字描述出來之後是不是很簡單?我們一般解決問題都是透過問題的描述,去寫相應的程式碼實現。
先來看這個例子:
package main
import "fmt"
func (n int) absoulte() int{
if n < 0 {
return -n
}
return n
}
func main)(){
num := 3
fmt.Println(num.absoulte())
}
執行結果
cannot define new methods on non-local type int
num.absoulte undefined (type int has no field or method absoulte) (exit status 1)
我們可以先用自訂型態來實現。
package main
import "fmt"
type myInt int
func (n myInt) absoulte() myInt{
if n < 0 {
return -n
}
return n
}
func main)(){
var num int = -3
fmt.Println(num.absoulte())
fmt.Println(num)
}
執行結果
3
-3
這邊要注意,上面的例子是用非指標型態定義方法,所以在呼叫 absoulute()
時,只是複製了變數 num 過去,並將結果回傳回來,原本的 num 並沒有更改。所以當印出 num 時才會是 -3。
如果想要更改原本的變數num,可以用指標的方式來實作:
package main
import "fmt"
type myInt int
func (n *myInt) absoulte() myInt{
if *n < 0 {
*n = -*n
}
return *n
}
func main)(){
var num int = -3
pointer := &num
fmt.Println(num.absoulte())
fmt.Println(num)
}
執行結果
3
3
我們再回頭看上面例子的 Method
setColor()
,有沒有發現有什麼不一樣呢?
這個 Method
的 receiver
是指向 Box
的指標,那為什麼是用指標 *Box
而不是 Box
本身呢?
首先,定義 setColor
主要的目的是想改變這個 Box
的顏色,如果不傳 Box
的指標,那麼 setColor()
接受的其實是 Box
的一個 copy,也就是說 Method
內對於顏色值的修改,其實只作用於 Box
的 copy,而不是真正的 Box
。所以我們需要傳入指標。
這裡可以把 receiver
當作 Method 的第一個參數來看,然後結合前面函式講解的傳值和傳參考就不難理解。
那是不是應該在 setColor()
中定義 *b.color = c
才對,因為需要讀取到指標相應的值。
沒錯!Go 中這兩種方式都是正確的,當你用指標去存取相應的欄位時(雖然指標沒有任何的欄位),Go 就知道你要透過指標去取得這個值。
也許細心的你會問這樣的問題,paintItBlack()
裡面呼叫 setColor()
的時候是不是應該寫成(&bl[i]).SetColor(BLACK)
,因為 setColor()
的 receiver
是 *Box
,而不是 Box
。
你又說對了,這兩種方式都可以,因為 Go 知道 receiver
是指標,他自動幫你轉了
也就是說:
如果一個
Method
的receiver
是*T
,你可以在一個T
型別的變數V
上面呼叫這個Method
,而不需要&V
去呼叫這個Method
。
類似的:
如果一個
Method
的receiver
是T
,你可以在一個T
型別的變數P
上面呼叫這個Method
,而不需要P
去呼叫這個Method
。
所以,不用擔心你是呼叫的指標的 Method
還是不是指標的 Method
,Go 知道你要做的一切。
前面幾章我們學習了欄位的繼承,那麼你也會發現 Go 的一個神奇之處,method 也是可以繼承的。如果匿名欄位實現了一個 method,那麼包含這個匿名欄位的 struct 也能呼叫該 method。
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名欄位
school string
}
type Employee struct {
Human //匿名欄位
company string
}
//在 human 上面定義了一個 method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
上面的例子中,如果 Employee 想要實現自己的 SayHi,怎麼辦?簡單,和匿名欄位衝突一樣的道理,我們可以在 Employee 上面定義一個 method,重寫了匿名欄位的方法。請看下面的例子
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名欄位
school string
}
type Employee struct {
Human //匿名欄位
company string
}
//Human 定義 method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee 的 method 重寫 Human 的 method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
透過這些內容,我們可以設計出基本的物件導向的程式了,但是 Go 裡面的物件導向是如此的簡單,沒有任何的私有、公有關鍵字,透過大小寫來實現(大寫開頭的為公有,小寫開頭的為私有),方法也同樣適用這個原則。