gopl-zh.github.com/ch5/ch5-06-1.md

2.5 KiB
Raw Blame History

5.6.1. 警告:捕獲迭代變量

本節將介紹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本身導致的而是因爲它們都會等待循環結束後再執行函數值。