捕获迭代变量

现在假设有一个程序必须创建一系列的目录之后又会删除它们。可以用一个包含函数变量的slice进行清理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var rmdirs []func()
for _, d := range tempDirs() {
dir := d
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}

//...

for _, rmdir := range rmdirs {
rmdir() //清理
}

在这段代码里循环变量d被赋值给新的局部变量dir,而不是直接去使用循环变量d,以下是错误示例

1
2
3
4
5
6
7
var rmdirs[] func()
for _, dir := range tempDirs() {
os.MkdirAll(dir, 0755)
rmdirs = append(rmdirs, func(){
os.RemoveAll(dir)
})
}

错误的原因在于,在Golang中循环里创建的所有函数变量共享相同的变量,也就是说在调用清理函数前dir已经被更新很多次了,而清理函数只是把最后一次的dir清理了,解决该问题的方法是引入一个内部变量,就像dir变量是一个和外部变量同名的变量,只不过是一个副本,这看起来似乎是多余的但实际上是程序能否正确运行的关键

1
2
3
for _, dir := range tempDirs() {
dir := dir //声明内部dir,并以外部dir初始化
}

这种问题不只存在于使用range的for循环里。在下面的循环中也因捕获索引变量i而导致同样的问题

1
2
3
4
5
6
7
8
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]) //错误
})
}

在Golang中迭代变量捕获的问题是很频繁的,因为这会推迟函数的执行时机,直到循环结束。所以要倍加留意在循环中的函数变量。