安裝本網站至主畫面:

[Go][Golang] 程式設計教學:函式 (Function)

PUBLISHED ON OCT 1, 2017 — PROGRAMMING
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    在本教程先前的文章中,大部分的程式碼都是放在主函式 (main function) 中,然而,隨著程式碼規模上升,這樣的方式漸不敷使用,像是程式碼上下捲動不易維護,也無法將程式碼分離在不同檔案中。程式設計者撰寫函式 (function) 以分離程式碼,函式也是程式碼共用的基礎。物件導向的方法 (method) 也是函式為基礎。

    使用函式

    到目前為止,我們已經使用一些函式,像是在 fmt 套件的 Println 函式,可在終端機中印出文字,math/rand 套件的 Intn 可以產生隨機的整數等。讀者會發現 Go 的公開函式都是以大寫開頭,這是 Go 語言的一項設計,使用大小寫來控管套件中函式和方法的權限,只有字母大寫的函式或方法才會公開給外界使用。

    撰寫函式

    以下範例撰寫一個簡單的 hello 函式,並呼叫三次:

    package main
     
    import (
        "fmt"
    )
     
    func hello() {
        fmt.Println("Hello World")
    }
     
    func main() {
        hello()
        hello()
        hello()
    }
    

    雖然這個函式相對簡單,我們可以從這個範例看出來,函式可以重覆再利用。

    函式也可以加入參數 (parameter),透過參數,我們可以改變函式的行為;如以下範例:

    package main
     
    import (
        "fmt"
    )
     
    func hello(name string) {
        fmt.Println(fmt.Sprintf("Hello %s", name))
    }
     
    func main() {
        hello("Michael")
        hello("Jenny")
        hello("Tommy")
    }
    

    函式運作後,也可以有回傳值 (return value),如以下範例:

    package main
     
    import (
        "log"
        "math"
    )
     
    func add(a float64, b float64) float64 {
        return a + b
    }
     
    func main() {
        if !(math.Abs(add(3, 2)-5.0) < (1.0 / 1000000)) {
            log.Fatal("Wrong value")
        }
    }
    

    多個回傳值

    Go 函式允許多個回傳值,如以下實例:

    package main
     
    import (
        "log"
    )
     
    func divmod(a int, b int) (int, int) {
        return a / b, a % b
    }
     
    func main() {
        m, n := divmod(5, 3)
     
        if !(m == 1) {
            log.Fatal("Wrong value m")
        }
     
        if !(n == 2) {
            log.Fatal("Wrong value n")
        }
    }
    

    多回傳值時常用於錯誤處理 (error handling) 中,我們將於後續文章中介紹。

    改變參數的函式

    搭配指標,函式也可以改變參數。在我們這個例子中,我們以函式搭配結構撰寫一個 C 風格的 Point 物件:

    package main
     
    import (
        "log"
    )
     
    type Point struct {
        x float64
        y float64
    }
     
    func Point_new(x float64, y float64) *Point {
        p := new(Point)
     
        p.x = x
        p.y = y
     
        return p
    }
     
    func Point_get_x(p *Point) float64 {
        return p.x
    }
     
    func Point_get_y(p *Point) float64 {
        return p.y
    }
     
    func Point_set_x(p *Point, x float64) {
        p.x = x
    }
     
    func Point_set_y(p *Point, y float64) {
        p.y = y
    }
     
    func main() {
        p := Point_new(0, 0)
     
        if !(Point_get_x(p) == 0) {
            log.Fatal("Wrong value")
        }
     
        if !(Point_get_y(p) == 0) {
            log.Fatal("Wrong value")
        }
     
        Point_set_x(p, 3.0)
        Point_set_y(p, 4.0)
     
        if !(Point_get_x(p) == 3.0) {
            log.Fatal("Wrong value")
        }
     
        if !(Point_get_y(p) == 4.0) {
            log.Fatal("Wrong value")
        }
    }
    

    由於 Go 支援物件的語法,實務上,我們不會用這種方法撰寫物件,本範例只是展示如何在函式中使用指標。

    不定長度參數

    細心的讀者可能會發現,Go 有些函式的參數長度是不固定的,像是 fmt 套件的 PrintfSprintf 函式,這兩個函式,第一個參數是做為模板的字串,第二個以後的參數就是我們要填入的值,而值的數量不是固定的。這是由於 Go 支援不定長度參數的特性,如以下範例:

    package main
     
    import (
        "log"
    )
     
    func sum(args ...float64) float64 {
        sum := 0.0
     
        for _, e := range args {
            sum += e
        }
     
        return sum
    }
     
    func main() {
        s := sum(1, 2, 3, 4, 5)
     
        if !(s == 15.0) {
            log.Fatal("Wrong value")
        }
    }
    

    不定參數傳入函式後,參數本身是一個切片,在函式中透過此切片即可取得個別的參數值。

    模擬預設變數

    Go 的函式本身不支援預設變數,但我們可以透過傳入結構的方式模擬預設變數,如下例:

    package main
     
    import (
        "log"
    )
     
    type Color int
     
    const (
        White Color = iota
        Black
        Green
        Yellow
    )
     
    type Size int
     
    const (
        Large Size = iota
        Middle
        Small
        ExtraLarge
    )
     
    type Clothes struct {
        color Color
        size  Size
    }
     
    type Param struct {
        Color Color
        Size  Size
    }
     
    func MakeClothes(param Param) *Clothes {
        c := new(Clothes)
     
        c.color = param.Color
        c.size = param.Size
     
        return c
    }
     
    func main() {
        // Clothes with custom parameters
        c1 := MakeClothes(Param{Color: Black, Size: Middle})
     
        if !(c1.color == Black) {
            log.Fatal("Wrong color")
        }
     
        if !(c1.size == Middle) {
            log.Fatal("Wrong size")
        }
     
        // Clothes with default parameters
        c2 := MakeClothes(Param{})
     
        if !(c2.color == White) {
            log.Fatal("Wrong color")
        }
     
        if !(c2.size == Large) {
            log.Fatal("Wrong size")
        }
    }
    

    有些讀者可能會想到用不定長度參數模擬預設參設,但筆者認為這不是好的模式,使用不定長度參數的話,需要多寫許多程式去檢查不同長度時的情境,而且參數位置是寫死的,無法像結構般靈活。

    函式重載

    Go 也不支援函式重載,用上一節的方式也可用來模擬函式重載,或是直接以不同函式來命名即可,如下例:

    package main
     
    import (
        "log"
    )
     
    func add(a float64, b float64) float64 {
        return a + b
    }
     
    func addOne(a float64) float64 {
        return add(a, 1)
    }
     
    func main() {
        if !(addOne(3) == 4) {
            log.Fatal("Wrong value")
        }
    }
    

    傳值呼叫 vs. 傳址呼叫

    基本上,如同 C 語言,所有 Go 的函式都是傳值呼叫 (call by value),而這個值有可能是基本型別或指標或其他型別。當我們傳遞指標時,我們會拷貝指標的位址,但不會拷貝指標所指向的值;當值很大時,傳遞指標比傳整個值有效率。所以,其實沒有傳址呼叫 (call by adress),這只是對初學指標的學習者易於記憶的一種詞語。另外,Go 沒有 C++ 的傳參考呼叫 (call by reference)。

    init 函式

    init 函式是一個特殊的函式,若程式碼內有 init 會在程式一開始執行的時候呼叫該函式,順序在 main 函式之前。通常 init 函式都是用來初始化一些外部資源。以下是一個摘自 Go 官方網站的例子:

    func init() {
        if user == "" {
            log.Fatal("$USER not set")
        }
        if home == "" {
            home = "/home/" + user
        }
        if gopath == "" {
            gopath = home + "/go"
        }
        // gopath may be overridden by --gopath flag on command line.
        flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
    }
    
    你或許對以下產品有興趣