天天看点

协程和延迟调用的实参的估值时刻

一个协程调用或者延迟调用的实参是在此调用发生时被估值的。更具体地说,

  • 对于一个延迟函数调用,它的实参是在此调用被推入延迟调用堆栈的时候被估值的。
  • 对于一个协程调用,它的实参是在此协程被创建的时候估值的。

一个匿名函数体内的表达式是在此函数被执行的时候才会被逐个估值的,不管此函数是被普通调用还是延迟/协程调用。

一个例子:

package main

import "fmt"

func main() {
    func() {
        for i := 0; i < 3; i++ {
            defer fmt.Println("a:", i)
        }
    }()
    fmt.Println()
    func() {
        for i := 0; i < 3; i++ {
            defer func() {
                fmt.Println("b:", i)
            }()
        }
    }()
}      

运行之,将得到如下结果:

a: 2
a: 1
a: 0

b: 3
b: 3
b: 3      

一个匿名函数中的循环打印出​

​2​

​、​

​1​

​和​

​0​

​这个序列,但是第二个匿名函数中的循环打印出三个​

​3​

​。 因为第一个循环中的​

​i​

​是在​

​fmt.Println​

​函数调用被推入延迟调用堆栈的时候估的值,而第二个循环中的​

​i​

​是在第二个匿名函数调用的退出阶段估的值(此时循环变量​

​i​

​的值已经变为​

​3​

​)。

我们可以对第二个循环略加修改(使用两种方法),使得它和第一个循环打印出相同的结果。

for i := 0; i < 3; i++ {
            defer func(i int) {
                // 此i为形参i,非实参循环变量i。
                fmt.Println("b:", i)
            }(i)
        }      

或者

for i := 0; i < 3; i++ {
      i := i // 在下面的调用中,左i遮挡了右i。
             // <=> var i = i
      defer func() {
        // 此i为上面的左i,非循环变量i。
        fmt.Println("b:", i)
      }()
    }      

同样的估值时刻规则也适用于协程调用。下面这个例子程序将打印出​

​123 789​

​。

package main

import "fmt"
import "time"

func main() {
    var a = 123
    go func(x int) {
        time.Sleep(time.Second)
        fmt.Println(x, a) // 123 789
    }(a)

    a = 789

    time.Sleep(2 * time.Second)
}      

  顺便说一句,使用​

​time.Sleep​

​​调用来做并发同步不是一个好的方法。 如果上面这个程序运行在一个满负荷运行的电脑上,此程序可能在新启动的协程可能还未得到执行机会的时候就已经退出了。 在正式的项目中,我们应该使用​​并发同步技术​​一文中列出的方法来实现并发同步。

下面来自go圣经一书中:

捕获迭代变量

本节,将介绍Go词法作用域的一个陷阱。请务必仔细的阅读,弄清楚发生问题的原因。即使是经验丰富的程序员也会在这个问题上犯错误。

考虑这个样一个问题:你被要求首先创建一些目录,再将目录删除。在下面的例子中我们用函数值来完成删除操作。下面的示例代码需要引入os包。为了使代码简单,我们忽略了所有的异常处理。

var rmdirs []func()
for _, d := range tempDirs() {
    dir := d // NOTE: necessary!
    os.MkdirAll(dir, 0755) // creates parent directories too
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}
// ...do some work…
for _, rmdir := range rmdirs {
    rmdir() // clean up
}      

你可能会感到困惑,为什么要在循环体中用循环变量d赋值一个新的局部变量,而不是像下面的代码一样直接使用循环变量dir。需要注意,下面的代码是错误的。

var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // NOTE: incorrect!
    })
}      

问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以dir为例,后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录。

通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量的副本。比如下面的变量dir,虽然这看起来很奇怪,但却很有用。

for _, dir := range tempDirs() {
    dir := dir // declares inner dir, initialized to outer dir
    // ...
}      
var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
    os.MkdirAll(dirs[i], 0755) // OK
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dirs[i]) // NOTE: incorrect!
    })
}