函式入門


在 Go 中要定義函式,是使用 func 來定義,其基本格式如下:

func funcName(param1 type1, param2 type2) (return1 type1, return2 type2) {
    // 一些程式碼...
    return value1, value2
}

定義函式

可以看到,Go 定義函式時,參數的型態宣告同樣地是放在名稱之後,如果多個參數有同樣的型態,那麼只要最右邊同型態的名稱右方加上型態就可以了,比較特別的地方在於,可以有兩個以上的傳回值,且傳回值可以設定名稱。

來看個簡單的函式定義,以下是個求最大公因數的函式定義:

package main

import "fmt"

func Gcd(m, n int) int {
    if n == 0 {
        return m
    } else {
        return Gcd(n, m%n)
    }
}

func main() {
    fmt.Printf("Gcd of 10 and 4: %d\n", Gcd(10, 4)) // 2
}

當只有一個傳回值且沒有宣告名稱時,傳回值的宣告可以不用使用 (),傳回值的名稱可以在函式中使用,傳回值名稱設定的值,會自動於函式 return 時傳回,例如:

package main

import "fmt"

func Gcd(m, n int) (gcd int) {
    if n == 0 {
        gcd = m
    } else {
        gcd = Gcd(n, m%n)
    }
    return
}

func main() {
    fmt.Printf("Gcd of 10 and 4: %d\n", Gcd(10, 4)) // 2
}

官方的建議是要宣告傳回值名稱,令程式可讀性更高(當然程式會變得囉嗦一些),對那些公開給套件外使用的函式(也就是首字大寫的函式),最好是宣告傳回值名稱。

多個傳回值

Go 中允許多個傳回值,例如,定義一個函式,可搜尋 slice 的元素中是否指定的子字串,若有就傳回元素索引位置與字串,若無就傳回 -1 與空字串:

package main

import "fmt"
import "strings"

func FirstMatch(elems []string, substr string) (int, string) {
    for index, elem := range elems {
        if strings.Contains(elem, substr) {
            return index, elem
        }
    }
    return -1, ""
}

func main() {
    names := []string{"Justin Lin", "Monica Huang", "Irene Lin"}
    if index, name := FirstMatch(names, "Huang"); index == -1 {
        fmt.Println("找不到任何東西")
    } else {
        fmt.Printf("在索引 %d 找到 \"%s\"\n", index, name)
    }
}

傳回多值時,指定給變數時必須依順序,若不需要某個傳回值,可以使用 _ 略過:

_, name := FirstMatch(names, "Huang")

另一種多值傳回的場合之一是錯誤處理,例如:

package main

import "fmt"
import "errors"

func Div(x, y int) (int, error) {
    if y == 0 {
        return 0, errors.New("division by zero")
    }
    return x / y, nil
}

func main() {
    if result, err := Div(10, 5); err == nil {
        fmt.Printf("10 / 5 = %d\n", result)
    } else {
        fmt.Println(err)
    }
}

若函式簽署上有傳回 error,應透過檢查其是否為 nil 來確認執行時是否有錯誤發生,這是 Go 的錯誤處理風格之一,例如,os.Open 的函式簽署是:

func Open(name string) (file *File, err error)

透過 os.Open 開啟檔案時的一個基本範例就是:

file, err := os.Open("file.go")
if err != nil {
    log.Fatal(err)
}

可變參數

在呼叫方法時,若方法的引數個數事先無法決定該如何處理?在 Go 中支援不定長度引數(Variable-length Argument),可以輕鬆的解決這個問題。直接來看示範:

package main

import "fmt"

func Sum(numbers ...int) int {
    var sum int
    for _, number := range numbers {
        sum += number
    }
    return sum
}

func main() {
    fmt.Println(Sum(1, 2))          // 3
    fmt.Println(Sum(1, 2, 3))       // 6
    fmt.Println(Sum(1, 2, 3, 4))    // 10
    fmt.Println(Sum(1, 2, 3, 4, 5)) // 15
}

可以看到,要使用不定長度引數,宣告參數時要於型態關鍵字前加上 ...,此參數本質上是個 slice,因此可以使用 for range 來走訪元素,可接受可變長度的參數只能有一個,而必須是最後一個參數。

雖然可接受可變長度引數的參數,本質上是個 slice,然而,若已經有個 slice,並不能直接傳遞給它,而必須使用 ... 展開,否則會發生錯誤:

package main

import "fmt"

func Sum(numbers ...int) int {
    var sum int
    for _, number := range numbers {
        sum += number
    }
    return sum
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Sum(numbers...)) // 15
}

函式與指標

Go 語言有指標,因此,在變數傳遞就多了一種選擇,直接來看個例子,以下的執行結果會顯示 1:

package main

import "fmt"

func add1To(n int) {
    n = n + 1
}

func main() {
    number := 1
    add1To(number)
    fmt.Println(number) // 1
}

這應該沒有問題,因為傳遞的是變數值n,函式中 n 的值加上 1 之後,再指定回給 n,這對 main 中的 number 變數毫無影響,因此函式結束後,顯示 number 的值,仍舊是 1。

那麼來看下面這個例子:

package main

import "fmt"

func add1To(n *int) {
    *n = *n + 1
}

func main() {
    number := 1
    add1To(&number)
    fmt.Println(number) // 2
}

這次使用了 &number 取得 number 的位址值再傳遞給 n,也就是傳遞了變數位址值n,函式中使用 *n 取得位址處的值,加上 1 後再將值存回原位址處,因此,透過 main 函式中的 number 取得的值,也會是加 1 後的值。