Go 语言匿名函数/闭包

在Go 程序中,像匿名函数,例如:func(x, y int) int { return x + y }。这样的函数不能独立存在,编译器会返回错误:non-declaration statement outside function body,但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3,4)

当然,您也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)

下面是一个计算从 1 到 1 百万 整数总和的匿名函数:

  1. func() {
  2. sum := 0
  3. for i := 1; i <= 1e6; i++ {
  4. sum += i
  5. }
  6. }()

参数列表的第一对括号()必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

下面的例子展示了如何将匿名函数赋值给变量并对其进行调用:

  1. package main
  2. import "fmt"
  3. func main() {
  4. f()
  5. }
  6. func f() {
  7. for i := 0; i < 4; i++ {
  8. g := func(i int) { fmt.Printf("%d ", i) }//此例子中只是为了演示匿名函数可分配不同的内存地址,在现实开发中,不应该把该部分信息放置到循环中。
  9. g(i)
  10. fmt.Printf(" - g is of type %T and has value %v\n", g, g)
  11. }
  12. }

输出:

  1. 0 - g is of type func(int) and has value 0x681a80
  2. 1 - g is of type func(int) and has value 0x681b00
  3. 2 - g is of type func(int) and has value 0x681ac0
  4. 3 - g is of type func(int) and has value 0x681400

我们可以看到变量 g 代表的是 func(int),变量的值是一个内存地址。

所以我们实际上拥有的是一个函数值:匿名函数可以被赋值给变量并作为值使用


练习:在 main 函数中写一个用于打印 Hello World 字符串的匿名函数并赋值给变量 fv,然后调用该函数并打印变量 fv 的类型。

匿名函数像所有函数一样可以接受或不接受参数。下面的例子展示了如何传递参数到匿名函数中:

  1. func (u string) {
  2. fmt.Println(u)
  3. }(v)

请学习以下示例并思考:函数 f返回时,变量 ret 的值是什么?

  1. package main
  2. import "fmt"
  3. func f() (ret int) {
  4. defer func() {
  5. ret++
  6. }()
  7. return 1
  8. }
  9. func main() {
  10. fmt.Println(f())
  11. }

结果:变量 ret 的值为 2,因为 ret++ 是在执行 return 1语句后发生的。

这可用于在返回语句之后修改返回的 error 时使用。

defer 语句和匿名函数

关键字 defer 经常配合匿名函数使用,它可以用于改变函数的命名返回值。

匿名函数还可以配合 go 关键字来作为 goroutine 使用。

匿名函数同样被称之为闭包(函数式语言的术语),它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装。另一个不错的应用就是使用闭包来完成更加简洁的错误检查。

应用闭包:将函数作为返回值

在示例 function_return.go 中我们将会看到函数 Add2Adder 均会返回签名为 func(b int) int 的函数:

  1. func Add2() (func(b int) int)
  2. func Adder(a int) (func(b int) int)

函数 Add2 不接受任何参数,但函数 Adder 接受一个 int 类型的整数作为参数。

我们也可以将 Adder 返回的函数存到变量中(function_return.go)。

  1. package main
  2. import "fmt"
  3. func main() {
  4. // make an Add2 function, give it a name p2, and call it:
  5. p2 := Add2()
  6. fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
  7. // make a special Adder function, a gets value 2:
  8. TwoAdder := Adder(2)
  9. fmt.Printf("The result is: %v\n", TwoAdder(3))
  10. }
  11. func Add2() func(b int) int {
  12. return func(b int) int {
  13. return b + 2
  14. }
  15. }
  16. func Adder(a int) func(b int) int {
  17. return func(b int) int {
  18. return a + b
  19. }
  20. }

输出:

  1. Call Add2 for 3 gives: 5
  2. The result is: 5

下例为一个略微不同的实现(function_closure.go):

  1. package main
  2. import "fmt"
  3. func main() {
  4. var f = Adder()
  5. fmt.Print(f(1), " - ")
  6. fmt.Print(f(20), " - ")
  7. fmt.Print(f(300))
  8. }
  9. func Adder() func(int) int {
  10. var x int
  11. return func(delta int) int {
  12. x += delta
  13. return x
  14. }
  15. }

函数 Adder() 现在被赋值到变量 f 中(类型为 func(int) int)。

输出:

  1. 1 - 21 - 321

三次调用函数 f 的过程中函数 Adder() 中变量 delta 的值分别为:1、20300

我们可以看到,在多次调用中,变量 x 的值是被保留的,即 0 + 1 = 1,然后 1 + 20 = 21,最后 21 + 300 = 321。闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。

这些局部变量同样可以是参数,例如之前例子中的 Adder(as int)

这些例子清楚地展示了如何在 Go 语言中使用闭包。

在闭包中使用到的变量可以是在闭包函数体内声明的,也可以是在外部函数声明的:

  1. var g int
  2. go func(i int) {
  3. s := 0
  4. for j := 0; j < i; j++ { s += j }
  5. g = s
  6. }(1000) // 将参数1000传递给函数literal.

这样闭包函数就能够被应用到整个集合的元素上,并修改它们的值。然后这些变量就可以用于表示或计算全局或平均值。


练习:不使用递归但使用闭包改写 [递归函数](/go-function/function-recursive.html “递归函数”) 章节中的斐波那契数列程序。

一个返回值为另一个函数的函数可以被称之为工厂函数,这在您需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:

  1. func MakeAddSuffix(suffix string) func(string) string {
  2. return func(name string) string {
  3. if !strings.HasSuffix(name, suffix) {
  4. return name + suffix
  5. }
  6. return name
  7. }
  8. }

现在,我们可以生成如下函数:

  1. addBmp := MakeAddSuffix(".bmp")
  2. addJpeg := MakeAddSuffix(".jpeg")

然后调用它们:

  1. addBmp("file") // returns: file.bmp
  2. addJpeg("file") // returns: file.jpeg

可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数,是函数式语言的特点,Go 语言也具有一些函数式语言的特性。闭包在 Go 语言中非常常见,常用于 goroutine管道操作。在后续教程中会看到相关的详细讲解。