在 Go 里,你每声明定义一个函数,就意味着产生了一个函数值。函数值就像其它类型的值一样,可以进行赋值,也可以作为函数的返回结果返回。
函数值在 Go 语言里被称为 first-class value (第一等值)。相比那些什么 int 类型的值,float 类型的值来说,函数值的地位就显的要高的多,而且也重要的多。
在讲解函数值前,需要普及一些基本的概念。
1. 基本概念
1.1 函数值与函数签名
函数值,既然是值,那么它就一定有类型。函数值的类型,也称为函数签名(signature). 举个例子:
func add(x int, y int) int {
return x + y
}
上面的函数 add 的函数值可以通过 add
取到,比如你可以:
val := add
这样就可以拿到 add 函数的值了。那 val
变量的类型(签名)是什么呢?这非常好区分,有一个简单的办法,把函数声明里所有的名字去掉(包括函数名,参数名,返回值名),就是函数类型了。比如上面 val 的类型就是:
func(int, int) int
当然了 add 也是函数值,它的类型也是上面那样。既然这样,那你就可以通过func(int, int) int
声明一个这样类型的变量:
var f func(int, int) int
f = add
如果有另一个函数:
func sub(x int, y int) int {
return x - y
}
sub 函数值的签名也是 func(int, int) int
,也就是说它和 add 函数拥有相同的函数签名,但是 sub 函数和 add 函数的函数值不同。
聪明的同学一定能联想到 C/C++ 中的函数类型了,比如在 C 里,你可以定义一个函数类型,再使用该函数类型去声明和定义新的函数指针变量了:
typedef int (*MyFunc)(int, int);
int add(int x, int y) {
return x + y;
}
MyFunc f = add;
1.2 函数值的特性
正如 int 类型的零值是 0,map 类型的零值是 nil 一样,函数类型的零值是 nil.
函数值是可以被调用的,就像调用普通函数一样。如果调用一个 nil 值的函数值,会引起 panic 错误。举几个调用函数值的例子:
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func sub(x int, y int) int {
return x - y
}
func main() {
var f func(int, int) int
f = add
fmt.Println(f(5, 4)) // Output: 9
f = sub
fmt.Println(f(5, 4)) // Output: 1
}
- 函数值是不能比较大小的,也不能互相比较相等。但是函数值可以和 nil 进行比较(函数值是一种引用类型)。例如上面的例子中的函数 add 和 sub:
fmt.Println(add == sub) // not ok! 函数值之间不能比较
// 下面的使用方法都是 ok 的
if add != nil { // ok
z := add(1, 2)
}
fmt.Println(f != nil) // ok
2. 匿名函数(Anonymous Function)
前面我们遇到的所有函数都是有名函数。有名函数,只能在包级作用域声明。例如下面的声明是错误的:
func main() {
// not ok! Go 里会报语法错误
func add(x, y int) int {
return x + y
}
}
但是如果函数没有名字,则可以在函数体内声明:
// 下面是合法的
func main() {
add := func(x, y int) int {
return x + y
}
}
有人会问,上面的函数不是有名字吗,叫 add。不,这不一样,我说的是函数名字,是指 func 关键字后面的名字。如果 func 关键字后面不写函数名,那就是一个匿名函数。在上面的例子里,我们只是将这个匿名函数的函数值赋值给了一个局部变量 add.
这样一来,我们又可以愉快的调用 add 函数了。
3. 闭包(closure)
闭包,我个人理解如下:
- 它是一个函数
- 这个函数有状态
通常我们使用的函数都是无状态的,也就是说,只要你给相同的输入,那这个函数的输出结果每次一定都是一样的。
而有状态的函数却不一样,你每次调用,返回的结果可能都不一样。举个例子:
package main
import "fmt"
func step(x int) func() int{
return func() int {
x++
return x
}
}
func main() {
f := step(10)
fmt.Println(f()) // Output: 11
fmt.Println(f()) // Output: 12
fmt.Println(f()) // Output: 13
fmt.Println(f()) // Output: 14
fmt.Println(f()) // Output: 15
}
在上面的例子中,函数值 f
就是一个闭包,因为每次调用它,返回的结果都不一样。它就是一个有状态的函数。它的原理很简单,匿名函数可以访问它所在的词法块内的变量。比如 step
函数内的匿名函数,就可以访问到 step
函数词法块内的局部变量 x
.
无论是哪种语言,闭包都是一种非常强大的技术。Go 天然支持了它,如虎添翼。
上面的例子从侧面说明了函数值不同于其它普通 int 类型值,函数值不仅仅是可以被调用,还存在状态!
4. 总结
- 这一篇非常重要
- 掌握函数值和函数类型的概念
- 掌握匿名函数
- 掌握闭包,理解什么是闭包