在介绍 Golang 中的函数闭包之前,需要了解到 Golang 中的函数也是值,可以由变量来定义声明,因此也可以像其他值一样传递、作为参数或返回值。

什么是函数闭包

而当一个函数作为变量在另一个函数中定义声明或是返回,并且引用了函数体之外的变量时,就发生了函数闭包。也就是说,闭包是一个函数值,它引用了其函数体之外的变量,并可以访问并修改其引用的变量,这个函数与其引用的函数体外的变量绑定到一起了。

在 Go 中,函数字面都是闭包

函数闭包的实例

这是官方 tour 中使用函数闭包的实现斐波那契数列的实例,可以由此简单地理解下函数闭包的使用。

package main

import "fmt"

func fibonacci() func() int {
	i := 0
	j := 1
	return func() int {
		res := i
		i, j = j, i + j
		return res
	}
}

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

函数闭包的好处

以下均是个人理解:

总的来说,函数闭包适合用在对一个函数频繁调用求值的情况,闭包函数与变量相关联,可以保存变量的中间状态,向主函数隐藏了中间变量的定义,避免了变量的泛滥误用

函数闭包的关键在于,闭包函数对于中间变量定义的隐藏和其中间状态的保持

如上面的斐波那契数列例子,如不使用闭包,除了定义 var i, j int 这两个中间变量来计算数列值,为了程序语义那么可能还需额外定义一个变量 var cur int 来保存当前的数列值,在主动跳出循环后才可继续使用数列的值。这时,在当前作用域中,就多出了三个只在当前代码段使用的变量。这会间接地导致导致变量泛滥和误用

函数闭包注意事项

一、由于中间状态的保持,所以需要额外注意当前闭包处于哪种状态,避免误用风险,更好地方式是,每次需要使用闭包函数的时候就定义新的闭包

二、当闭包函数引用了全局作用域的变量时,需要特别小心,因为其他代码对于全局变量的访问均会影响闭包的下一次状态,如:

package main

import "fmt"

var add int = 1

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x + add
		return sum
	}
}

func main() {
	pos := adder()
	for i := 0; i < 10; i++ {
		// 把 add++ 注释掉试一试
		add++
		fmt.Println(pos(i), add)
	}
}

经过良好设计的闭包函数,其执行的上下文环境应该是完全封闭的,因此,想要完全避免这个问题,就是不在闭包函数中引用全局变量

三、前面提到闭包函数与其所引用的变量绑定起来,但是这个绑定并非是瞬时的。再看个例子:

package main

import "fmt"

func foo(x int) []func() {
    var fs []func()
    values := []int{1, 2, 3, 4}
    for _, val := range values {
        fs = append(fs, func() {
            fmt.Println(x+val)
        })
    }
    return fs
}

func main() {
    fs := foo(6)
    for _, f := range fs {
        f()
    }
    // 会输出四个 10
}

也就是说,在声明一个闭包函数时,并不会直接将当前的环境状态绑定给它,在调用闭包函数时,它才会获取最新的外部环境。在这个例子中,val 的最后最新值为 4,所以 fs 中的闭包函数所绑定的 val 在调用时都会使用这一个值。

这也称为闭包的延迟绑定。而在并发程序中,如使用了 goroutine 等,在异步或并发执行,同样会触发延迟绑定,需要特别注意:Go Routine的匿名函数的延迟绑定本质就是闭包的延迟绑定

个人认为,想要彻底规避延迟绑定问题是不可能的,只有在代码书写上面下一些功夫,如:

  • 使用时才定义闭包
  • 尽量少用闭包

总之一句话:

在Go中,函数字面都是闭包,需要尽量保证函数内引用变量的生命周期函数的活动时间相同。

日后有新的认识再来补充,over~