Go中的函数除了可以声明入参之外,还可以声明结果参数(即返回值)。函数可以没有参数或接受多个参数,并且类型在变量名之后。当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

一 函数声明

语法:
func 函数名 (签名 类型)[([结果签名 ]类型)]][函数体]

例如:
示例1:

// 普通函数
func info() {
    fmt.Println("some tips.")
}

示例2:

// 2个入参x, y; 1个未命名结果参数(int类型)
func add(x int, y int) int {
    return x + y // 必须要有return
}

上边的示例2还可以简写成这样:
示例3:

// 2个入参x, y; 1个未命名结果参数(int类型)
func add(x, y int) int {
    return x + y
}
注意第2行写法变化

1.1 多返回值

Go一个与众不同的特点是,函数和方法可以返回多个值:

func swap(x, y string) (string, string) {
    return y, x
}

上述示例接受两个字符串类型的参数,并将其交换返回。

1.2 可命名结果参数

Go函数的返回“参数”(或称为结果“参数”)可以给出名称并用作常规变量,就像传入参数一样。如果给出名称,它们将在在函数开始时被初始化为其类型的零值;如果函数执行不带参数的return语句(即直接返回),结果参数的当前值将用作返回值。

例如,上一小节的示例还可以这样写:

func swap(x, y string) (m string, n string) {
    m = y
    n = x

    return m, n
}

直接返回语句应当仅用在短函数中。在长的函数中它们会影响代码的可读性。
另外,名称不是强制的,但可以使代码更简短清晰,它们本身就是文档。

因为命名结果被初始化并与简单的return相关联,因此它们可以更简洁,例如io.ReadFull:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
    // 等价于 return n, err
}

如果函数的签名声明了结果参数,则函数体的语句列表必须以终止语句(return)结束:
示例1:

// 2个入参x, y; 1个结果参数(返回值,int类型)
func add(x int, y int) int {
    return x + y // 必须要有return
}

示例2:

func IndexRune(s string, r rune) int {
    for i, c := range s {
        if c == r {
            return i
        }
    }
    // 无效: 此处缺少return语句
}

1.3 无函数体的函数

函数声明可以省略函数体。这样的声明为Go外部实现的函数提供签名,例如:

func min(x int, y int) int {
    if x < y {
        return x
    }
    return y
}

func flushICache(begin, end uintptr)  // 此函数由外部实现

二 函数类型

函数类型表示具有相同参数和结果类型的所有函数的集合。

在参数或结果列表中,名称(标识符列表)必须要么全部提供,要么全部省略。如果提供了,则每一个名称代表指定类型的一个条目(参数或结果),并且签名中所有非空白名称必须是唯一的。如果不提供,则每种类型代表该类型的一个条目。参数和结果列表应始终带括号,除非只有一个未命名结果参数,则可以不带括号。

func()          // 无入参,无结果参数
func(x int) int // 1个入参,1个未命名结果参数
func(x int) (y int, string) // 语法错误:命名和未命名结果参数混合使用
func(a, _ int, z float32) bool
func(a, b int, z float32) bool
func(prefix string, values ...int)                           // values为可变参数
func(a, b int, z float64, opt ...interface{}) (success bool) //4个入参,1个已命名结果参数;其中入参中含一个可变参数
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T) //函数类型的结果参数

2.1 函数值

函数也是值,它们可以像其它值一样传递,函数值的类型是函数类型。函数值可以用作函数的参数或返回值。
例如:

package main

import (
    "fmt"
    "math"
)

// compute函数的入参fn,是一个函数类型的参数;
// 该函数类型同时又有两个入参和一个返回值
func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    // 将函数作为值传入函数
    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
    // 匿名函数值
    fmt.Println(compute(func(x, y float64) float64 { return x + y }))
}

2.2 函数的闭包

Go函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

例如,下面的函数 adder 返回一个闭包。每个闭包都被绑定在其各自的sum变量上:

package main

import "fmt"

func adder() func(int) int {
    sum := 0

    return func(x int) int {
        sum += x
        fmt.Println("sum:", sum)
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 3; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

2.3 延伸:斐波那契闭包

普通版

package main

import "fmt"

// 返回一个“返回int的函数”
func fibonacci() func(int) int {
    j := 0
    k := 1
    z := 0

    return func(x int) int {
        if x < 2 {
            return x
        }

        z = j + k
        j = k
        k = z
        return z
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 15; i++ {
        fmt.Println(f(i))
    }
}

借助平行赋值时的求值顺序

package main

import "fmt"

// 返回一个“返回int的函数”
func fibonacci() func() int {
    // 费波那契数列由0和1开始
    f1, f2 := 0, 1

    return func() int {
        // 之后的费波那契系数由之前的两数相加得出
        f1, f2 = f2, f1+f2
        return f2 - f1
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 15; i++ {
        fmt.Println(f())
    }
}

另外,在下一篇文章中,我们还可以看到借助defer语句实现的斐波那契闭包。

三 调用

给定一个F类型的表达式ff(a1, a2, … an),用参数a1, a2, … an调用f。除一种特殊情况外,这些参数必须是可赋值给F类型参数的单值表达式,并且在函数调用前进行求值。表达式f的类型即F的结果类型。方法调用类似,但方法本身被指定为方法接收器类型的值的选择器。

math.Atan2(x, y)  // 函数调用
var pt *Point
pt.Scale(3.5)     // 接收器为pt的方法调用
方法就是特殊的函数,后续文章中会介绍。

函数调用中,函数值和参数按正常顺序求值,计算完成后,调用的参数通过值传递给函数,然后被调用的函数开始执行。函数返回时,函数的返回参数也通过值传递给调用方。

函数类型的零值为nil,调用nil函数值会导致运行时恐慌。

上边提到,作为一种特殊情况,如果一个函数或方法g的返回值在数量上等于另一个函数或方法f的入参,并且可以单独赋值给f,那么调用f(g(parameters_of_g))首先会将g的返回值按顺序绑定到f的参数,然后再调用f。此时f的调用不能包含除g的调用之外的参数,并且g必须至少具有一个返回值。如果f有类似final ...的可变参数(见下文),则将g的返回值优先赋值给常规参数,剩余的返回值继续赋值给可变参数。

func Split(s string, pos int) (string, string) {
    return s[0:pos], s[pos:]
}

func Join(s, t string) string {
    return s + t
}

if Join(Split(value, len(value)/2)) != value {
    log.Panic("test fails")
}

下面的示例中,Split函数返回3个字符串值,而Join函数有2个参数,其中,Split函数的第一个返回值会赋值给Join函数的第一个参数sSplit函数剩余的2个返回值将全部赋值给可变参数t

package main

import (
    "fmt"
)

func main() {
    fmt.Println("可变参数赋值演示:")

    s1 := "hello,Golang"

    fmt.Println(Join(Split(s1, 6)))
}

func Split(s string, pos int) (string, string, string) {
    if pos > len(s) {
        return "", "", "No enough strings"
    }

    return "内容:", s[0:pos], s[pos:]
}

func Join(s string, t ...string) string {
    fmt.Println("s: " + s)
    fmt.Println("t:", t)
    for _, value := range t {
        s += value
    }
    return s
}

3.1 可变参数

函数或方法签名中最后一个传入的参数可以具有...T的类型。这样的参数称为可变参数,并且可以使用该参数的零个或多个参数来调用。其中,该参数的类型为[]T,即可变参数是一个切片类型。向可变参数传递值会生成一个新的切片,其底层包含一个新的数组,数组的元素即为传递的参数。因此,切片的长度和容量就是可变参数的实际数量,并且每次调用都可能不一样。

package main

import (
    "fmt"
)

func main() {
    fmt.Println(Join("Hello, program languages:", "Golang", "Java", "PHP"))
}
func Join(s string, t ...string) string {
    for _, value := range t {
        s += " " + value
    }
    return s
}
如果可变参数没有传参,则该参数为nil

如果最后一个参数(实参)可赋值给切片类型[]T,则将该实参后跟...传递给...T参数(形参)可以保持其值不变。这种情况下,不会创建新的切片。

示例:

s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...) // Greeting的可变参数与s具有相同的底层数组。

参考:
https://golang.org/doc/effective_go.html#functions
https://golang.org/ref/spec#Calls
https://zh.wikipedia.org/wiki/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97

文章目录