在 Go 中使用方法

已完成

Go 中的方法是一種特殊類型的函式,其中最明顯的差異,就是您必須在函式名稱之前加設參數。 這個額外的參數稱為接收端

當您想要將函式分組,以及將函式繫結到自訂類型時,就可以使用方法。 Go 的此一作法,類似於在其他程式設計語言中建立類別,因為其可以讓您從物件導向程式設計 (OOP) 模型 (例如內嵌、多載及封裝) 中實作某些功能。

我們先從宣告方法開始說起,讓您了解方法在 Go 中為何如此重要。

宣告方法

截到目前為止,您只將結構當成可以在 Go 中建立的另一種自訂類型使用。 在本課程模組中,您將學習如何利用新增方法,將行為新增至您所建立的結構。

宣告方法的語法如下所示:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

但您必須先建立結構,才能宣告方法。 假設您要建立幾何套件,並在該套件中建立稱為 triangle 的三角形結構。 此外,您也想使用方法來計算該三角形的周長。 您可以依照下列所示,在 Go 中描述該三角形:

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

儘管此結構看起來像是一般結構,但 perimeter() 函式在函式名稱之前加設了參數類型 triangle。 這接收端表示當您使用此結構時,可用如下方式呼叫函式:

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

若您嘗試使用一般的方式呼叫 perimeter() 函式,該函式將無法運作,因為函式的簽章會指出需要接收端。 呼叫該方法的唯一方式是先宣告結構,這可將該方法的存取權授與您。 只要方法屬於不同的結構,您甚至可以擁有相同名稱的方法。 例如,您可以使用 perimeter() 函式宣告 square 結構,如下所示:

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

當您在執行上述程式碼時,若未出現任何錯誤,將會得到下列輸出:

Perimeter (triangle): 9
Perimeter (square): 16

編譯器會依據接收端類型,從兩個對 perimeter() 函式的呼叫中,決定所要呼叫的函式。 這行為有助確保所有套件中的函式一律使用短名稱與一致性,並可避免將套件名稱包含作為前置詞。 我們將在下個單元中探討介面時,討論為何這項行為可能很重要。

方法中的指標

有時候方法需要更新變數。 或者,如果方法的引數太大,您可能想要避免複製它。 在這些情況下,您必須使用指標傳遞變數的位址。 在先前的課程模組中,我們曾在探討指標時說過,每當您在 Go 中呼叫函式時,Go 都會複製每個引數值供其使用。

當您需要更新方法中的接收端變數時,Go 也會執行相同的動作。 例如,假設您想要建立新的方法,讓三角形的大小加倍。 您必須在接收端變數中使用指標,如下所示:

func (t *triangle) doubleSize() {
    t.size *= 2
}

您可以如下所示,證明此方法確實有效:

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

當您執行上述程式碼時,應會得到下列輸出:

Size: 6
Perimeter: 18

當方法只要存取接收器的資訊時,就無須在接收端變數中使用指標。 但是根據 Go 的慣例,若結構中有任何方法具有指標接收端,則該結構中的所有方法全都必須具有指標接收端。 即使該結構的方法並不需要此項目。

宣告其他類型的方法

方法有一個重要的層面,就是可以將其定義成任何類型,而不僅僅是像結構這樣的自訂類型。 不過,您無法為屬於另一個套件之類型的結構進行定義。 因此,您無法建立基本類型的方法,例如 string

但可以使用舊版建立基本類型的自訂類型,然後再將該類型當作基本類型使用。 舉例來說,假設您想要建立方法,將字串從小寫字母轉換成大寫。 您可以撰寫類似如下的內容:

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

當您執行上述程式碼時,會得到如下的輸出:

Learning Go!
LEARNING GO!

請留意您在第一次列印新物件 s 的值時,可以將其當作字串般地使用。 而當您呼叫 Upper 方法時,s 就會列印字串類型的所有大寫字母。

內嵌方法

在先前的課程模組中,您已了解您可以在一個結構中使用屬性,再將相同的屬性內嵌於另一個結構中。 也就是說,您可以重複使用結構中的屬性,一來避免反覆執行相同的動作,二來可以您的程式碼基底中保持一致性。 類似的概念也適用於方法。 即使接收端不同,也可以呼叫內嵌結構的方法。

例如,假設您想要使用邏輯來建立新的三角形結構,以加入色彩。 此外還想要繼續使用先前宣告的三角形結構。 彩色的三角形結構看起來會像這樣:

type coloredTriangle struct {
    triangle
    color string
}

您可以將 coloredTriangle 結構初始化,再從 triangle 結構呼叫 perimeter() 方法 (甚至可以存取其欄位),如下所示:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

繼續將前述變更加入您的程式中,從而了解內嵌如何運作。 當您使用與上一個方法類似的 main() 方法執行程式時,應該會得到下列輸出:

Size: 3
Perimeter 9

您若是熟悉 OOP 語言 (例如 JAVA 或 C++),可能會認為 triangle 結構看起來像基底類別,coloredTriangle 就像子類別 (例如繼承),但實則不然。 為什麼?事實上,Go 編譯器會建立包裝函式方法來升級 perimeter() 方法,看起來像這樣:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

請留意接收器是 coloredTriangle,會從三角形欄位呼叫 perimeter() 方法。 好消息是,您並不需要建立上述方法。 儘管您可以自己建立,但就交給 Go 為您代勞吧。 上述範例僅供學習之用。

多載方法

讓我們再回到前文討論的 triangle 範例。 若您想要變更 coloredTriangle 結構中 perimeter() 方法的實作方式會如何? 您不能有兩個同名的函式。 但是因為方法需要額外的參數 (接收端),所以只要該方法專屬於您要使用的接收端,就可以使用同名的方法。 利用這種區別就是您多載方法的方式。

換句話說,若您想要變更其行為,可以撰寫剛才討論的包裝函式方法。 若彩色三角形的周長是一般三角形周邊的兩倍,則程式碼會如下所示:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

現在,若不變更您先前所撰寫之 main() 方法中的任何其他內容,看起來會像這樣:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

當您執行該方法時,會得到不同的輸出:

Size: 3
Perimeter 18

但是,若您仍然需要 triangle 從結構呼叫 perimeter() 方法,可以藉由明確地存取該方法來完成此動作,如下所示:

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

當您執行此程式碼時,應會得到下列輸出:

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

您可能已經注意到,在 Go 中,您可以覆寫方法,而且如有需要,仍可以存取原始方法。

方法中的封裝

封裝表示方法無法存取物件的呼叫端 (用戶端)。 一般而言,在其他程式設計語言中,您可以將 privatepublic 關鍵字放置於方法名稱之前。 在 Go 中,您只需要使用大寫的識別項,就能將方法設為公用,而使用非大寫識別項,就能將方法設為私人。

在 Go 中,封裝只在套件之間有效。 換句話說,您只能隱藏另一個套件的實作詳細資料,而不能隱藏套件本身。

請建立新的套件 geometry 來試試看,並在套件中移動三角形結構,如下所示:

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

您可以使用上述套件,如下所示:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

您應會得到下列輸出:

Perimeter 18

若您嘗試從 main() 函式呼叫 size 欄位或 doubleSize() 方法,將會導致程式發生問題,如下所示:

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

當您執行上述程式碼時,會發生下列錯誤:

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)