函数

寒江蓑笠翁大约 11 分钟

函数

在Go中,函数是一等公民,函数是Go最基础的组成部分,也是Go的核心。例如启动函数main

package main

import "fmt"

func main() {
	fmt.println("Hello 世界!")
}

声明

函数的声明格式如下

func 函数名([参数列表]) [返回值] {
	函数体
}

函数可以直接通过func关键字来声明,也可以声明为一个字面量,也可以作为一个类型。

// 直接声明
func DoSomething() {

}

// 字面量
var doSomthing func()

// 类型
type DoAnything func()

函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子

func Sum(a, b int) int {
   return a + b
}

函数名称Sum,有两个int类型的参数ab,返回值类型为int

提示

在Go中,不支持函数重载。


参数

Go中的函数参数可以有名称,也可以没有名称。例如在声明一个函数字面量时可以省略名称,但是在赋值时依旧需要名称。

var sum func(int,int) int

sum = func(a int, b int) int {
   return a + b
}

对于一些类型相同且相邻的参数而言,可以只声明一次类型。例如

// a,b,c都是int类型的参数,所以只需要声明一次类型
func max(a, b, c int) int {
	if a < b {
		a, b = b, a
	}
	if a < c {
		a, c = c, a
	}
	return a
}

变长参数可以接收0个或多个值,必须声明在参数列表的末尾。

func max(args ...int) int {
   max := math.MinInt64
   for _, arg := range args {
      if arg > max {
         max = arg
      }
   }
   return max
}

提示

Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值


返回值

Go中的返回值也可以有名称,并且可以拥有多个返回值。当函数只有一个返回值且没有名称时如下例

func Sum(a, b int) int {
   return a + b
}

如果有多个返回值则需要加上括号,例如

func Div(a, b float64) (float64, error) {
	if a == 0 {
		return math.NaN(), errors.New("0不能作为被除数")
	}
	return a / b, nil
}

如果返回值有名称,也需要加上括号。

func Sum(a, b int) (ans int) {
   return a + b
}

也可以如下,不管返回值是否命名,优先级最高的永远都是return关键字后跟随的值。

func Sum(a, b int) (ans int) {
	ans = a + b
	return // 等价于 return ans
}

匿名函数

匿名函数只能在函数内部存在,匿名函数可以简单理解为没有名称的函数,例如

func main() {
   func(a, b int) int {
      return a + b
   }(1, 2)
}

或者当函数参数是一个函数类型时,这时名称不再重要,可以直接传递一个匿名函数

func main() {
	DoSum(1, 2, func(a int, b int) int {
		return a + b
	})
}

func DoSum(a, b int, f func(int, int) int) int {
	return f(a, b)
}

闭包

闭包(Closure)这一概念,在一些语言中又被称为Lamda表达式,经常与匿名函数一起使用,函数 + 环境引用 = 闭包。看一个例子:

func main() {
	sum := Sum(1, 2)
	fmt.Println(sum(3, 4))
	fmt.Println(sum(5, 6))
}

func Sum(a, b int) func(int, int) int {
	return func(int, int) int {
		return a + b
	}
}
3
3

在上述代码中,无论传入什么数字,输出结果都是3,稍微修改一下代码

func main() {
   sum := Sum(5)
   fmt.Println(sum(1, 2))
   fmt.Println(sum(1, 2))
   fmt.Println(sum(1, 2))
}

func Sum(sum int) func(int, int) int {
   return func(a, b int) int {
      sum += a + b
      return sum
   }
}
8
11
14

匿名函数引用了参数sum,即便Sum函数已经执行完毕,虽然已经超出了它的生命周期,但是对其返回的函数传入参数,依旧可以成功的修改其值,这一个过程就是闭包。事实上参数sum已经逃逸到了堆上,只要其返回值函数的生命周期没有结束,就不会被回收掉。


利用这一特性,可以非常简单的实现一个求费波那契数列的函数,代码如下

func main() {
	fib := Fib()
	for i := 0; i < 10; i++ {
		fmt.Println(fib())
	}
}

func Fib() func() int {
	a, b := 1, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}

输出为

1
2 
3 
5 
8 
13
21
34
55
89

延迟调用

defer关键字描述的一个匿名函数会在函数返回之前执行。

func main() {
	Do()
}

func Do() {
	defer func() {
		fmt.Println("1")
	}()
	fmt.Println("2")
}
2
1

当有多个defer语句时,会按照后进先出的顺序执行。

func main() {
   Do()
}

func Do() {
   defer func() {
      fmt.Println("1")
   }()
   defer func() {
      fmt.Println("2")
   }()
   defer func() {
      fmt.Println("3")
   }()
   defer func() {
      fmt.Println("4")
   }()
   fmt.Println("2")
   defer func() {
      fmt.Println("5")
   }()
}
2
5
4
3
2
1

延迟调用通常用于释放文件资源,关闭连接等操作,另一个常用的写法是用于捕获panic


参数预计算

对于延迟调用有一些小坑,比如下面这个例子

func main() {
	defer fmt.Println(Fn1())
	fmt.Println("3")
}

func Fn1() int {
	fmt.Println("2")
	return 1
}

这个坑还是非常隐晦的,笔者以前就因为这个坑,半天排查不出来是什么原因,可以猜猜输出是什么,答案如下

2
3
1

可能很多人认为是下面这种输出

3
2
1

按照使用者的初衷来说,fmt.Println(Fn1())这部分应该是希望它们在函数体执行结束后再执行,fmt.Println确实是最后执行的,但Fn1()是在意料之外的,下面这个例子的情况就更加明显了。

func main() {
	var a, b int
	a = 1
	b = 2
	defer fmt.Println(sum(a, b))
	a = 3
	b = 4
}

func sum(a, b int) int {
	return a + b
}

它的输出一定是3而不是7,如果使用闭包而不是延迟调用,结果又不一样了

func main() {
	var a, b int
	a = 1
	b = 2
	f := func() {
		fmt.Println(sum(a, b))
	}
	a = 3
	b = 4
	f()
}

闭包的输出是7,那如果把延迟调用和闭包结合起来呢

func main() {
	var a, b int
	a = 1
	b = 2
	defer func() {
		fmt.Println(sum(a, b))
	}()
	a = 3
	b = 4
}

这次就正常了,输出的是7。下面再改一下,没有闭包了

func main() {
	var a, b int
	a = 1
	b = 2
	defer func(num int) {
		fmt.Println(num)
	}(sum(a, b))
	a = 3
	b = 4
}

输出又变回3了。通过对比上面几个例子可以发现,跟在defer关键字后面的函数,它的参数在一开始就被计算好了

defer fmt.Println(sum(a,b))

其实就是

defer fmt.Println(3)

go不会等到最后才去调用sum函数,这也就导致了第一个例子中的奇怪现象,对于这种情况,尤其是在延迟调用中将函数返回值作为参数的情况尤其需要注意避免。