1. 程式人生 > 實用技巧 >golang捕獲迭代變數

golang捕獲迭代變數

本節,將介紹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
    // ...
}

這個問題不僅存在基於range的迴圈,在下面的例子中,對迴圈變數i的使用也存在同樣的問題

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

如果你使用go語句(第八章)或者defer語句(5.8節)會經常遇到此類問題。這不是go或defer本身導致的,而是因為它們都會等待迴圈結束後,再執行函式值。