天天看點

協程和延遲調用的實參的估值時刻

一個協程調用或者延遲調用的實參是在此調用發生時被估值的。更具體地說,

  • 對于一個延遲函數調用,它的實參是在此調用被推入延遲調用堆棧的時候被估值的。
  • 對于一個協程調用,它的實參是在此協程被建立的時候估值的。

一個匿名函數體内的表達式是在此函數被執行的時候才會被逐個估值的,不管此函數是被普通調用還是延遲/協程調用。

一個例子:

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!
    })
}