【在主画面加入捷径】
       
【选择语系】
繁中 简中

[Golang] 程序设计教学:撰写函式 (Function)

【赞助商连结】

    在本教程先前的文章中,大部分的程序代码都是放在主函式 (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")
    }