Golang: Language: Closure

16th December 2021 at 4:20pm

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,但是实际上 msgpass 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 可以读也可以写。

参考