go词法作用域陷进

问题

// 创建一些目录,再将目录删除
// 错误写法
var rmdirs []func()
for _, dir := range tempDirs() {
  os.MkdirAll(dir, 0755)
  rmdirs = append(rmdirs, func(){
    os.RemoveAll(dir)
})
}

// 正确写法
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
问题的原因在于循环变量的作用域。在上面的程序中,for循环语句引入了新的词法块,
循环变量dir在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。
需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一个时刻的值。以dir为例
后续的迭代会不断更新dir的值,当删除操作执行时,for循环已完成,dir中存储的值
等于最后一次迭代的值。这意味着,每次对os.RemoveAll的调用删除的都是相同的目录
通常,为了解决这个问题,我们会引入一个与循环变量同名的局部变量,作为循环变量
的副本。比如下面的变量dir,虽然看起来很怪,但很有用
for _, dir := range tempDirs(){
  dir := dir
}

当然这个问题不仅存在基于range循环,下面的例子中,对循环变量i的使用也存在同样的问题
var rmdirs []func()
dirs := tempDirs()
for i := 0;i<len(dirs);i++{
  os.MkdirAll(dirs[i], 0755)
  rmdirs = append(rmdirs, func(){
    os.RemoveAll(dirs[i])
})
}

如果你使用go语句或者defer语句会经常遇到此类问题。这不是go或defer本身导致的,
而是因为它们都会等待循环结束后,再执行函数值。
上一篇:【LeetCode】322. 零钱兑换


下一篇:《UNIX网络编程 卷1:套接字联网API(第3版)》——8.2 recvfrom和sendto函数