Closure 即匿名函数。在代码中经常会看到 go / defer 接着一个匿名函数:
go func() {
// ...
}()
defer func() {
// ...
}()
其中 func() {}
部分即是 closure。
Closure 的一大作用时,它可以使用在其他作用域的变量。对于一个普通函数:
func SayHello() {
msg := "hello"
fmt.Println(msg)
}
msg
的作用域仅在 SayHello
函数体内,在外部是无法访问的。但是:
func SayHello() {
msg := "hello"
go func() {
fmt.Println(msg)
}()
}
在这个函数中,closure 中使用了外部定义的 msg
变量,而且这个变量在 SayHello
执行结束后仍然可以被访问。这种变量叫 free variable。
Here's the tricky part。如果我在闭包中修改 msg
的值,也会影响 msg
所在的作用域吗?即 SayHello
函数中能否看到修改后的值?答案是可以的。下面这段代码会演示这个效果,但它不是正确的代码,因为它存在 data race:
func SayHello() {
msg := "hello"
go func() {
msg = "world"
}()
time.Sleep(100 * time.Millisecond)
fmt.Println(msg)
}
func main() {
SayHello()
}
这段代码会打印 world
。
这与习惯的认识有些区别。一般认为,函数中给一个变量赋值,仅仅可能影响到这个函数本身的执行,但不会影响到函数外部的数据;但是在闭包中并不是这样,虽然是赋值给 msg
,但是实际上 msg
是 pass by reference 的(可以理解为传的是指针)。上面的代码可以改写成:
func func1(msg *string) {
*msg = "world"
}
func SayHello() {
msg := "hello"
go func1(&msg)
time.Sleep(100 * time.Millisecond)
fmt.Println(msg)
}
这个特点结合 loop variable 可能导致其他问题,在 这里 有描述。
最佳实践
对于有 free variable 的 closure,如果是:
- 使用了当前协程的变量,但是在新协程中运行:那么对 free variable 应该只读不写;比较罕见的情况下需要写时,也应该使用同步原语。
- 在当前协程中运行(比如
defer
语句,比如作为函数对象被执行):那么对 free variable 可以读也可以写。