golang捕獲迭代變數
阿新 • • 發佈:2020-08-19
本節,將介紹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本身導致的,而是因為它們都會等待迴圈結束後,再執行函式值。