range 踩坑小记——为啥删不掉文件夹?

问题

最近在学 go ,自己做了一个文件夹创建之后再删除的练习,代码如下:

package main

import (
    "fmt"
    "os"
)

//建立三个临时文件夹 a b c
func tempDirs() []string {
    return []string{"a", "b", "c"}
}

func main() {

    //以队列的方式来存放删除操作
    var rmdirs []func()

    for _, dir := range tempDirs() {
        os.MkdirAll(dir, 0755)
        rmdirs = append(rmdirs, func() {
            fmt.Println("准备删除文件夹", dir)
            os.RemoveAll(dir)
        })
    }

    //做点别的事情

    for _, rm := range rmdirs {
        rm()
    }

}

表现

运行起来之后发现,只删除的文件夹 c,那么问题来了为啥不是按照预期依次删除 a b c 呢?

原因

这里不卖关子了,直接贴原因出来:

  1. 这里就是因为 range 循环的时候,只是拷贝了循环对象中的元素值出来,放到了临时变量当中,这个临时变量的地址是不变的,循环结束的时候,该 dir 临时变量存放的就是 c。
  2. 然后在我们 append 进匿名函数中的时候,这个 dir 变量实际上是把地址放到函数体内部,后续执行的时候就直接读取这个函数体内部变量的地址。因为该地址最后存放的值就是c,所以后面我们循环执行的时候删除的就是 c。

那么应该如何验证以上两条结论呢?

先验证原因1,这里直接在循环中打印 dir 的地址就好了,改写例程如下:

package main

import (
    "fmt"
    "os"
)

//建立三个临时文件夹 a b c
func tempDirs() []string {
    return []string{"a", "b", "c"}
}

func main() {

    //以队列的方式来存放删除操作
    var rmdirs []func()

    for _, dir := range tempDirs() {
      //此处打印 dir 地址
      fmt.Println("dir 的地址是 ",&dir)
        os.MkdirAll(dir, 0755)
        rmdirs = append(rmdirs, func() {
            fmt.Println("准备删除文件夹", dir)
            os.RemoveAll(dir)
        })
    }

    //做点别的事情

    for _, rm := range rmdirs {
        rm()
    }

}

运行之后可以看到类似如下输出:

dir 的地址是 0x10b44100 dir 的地址是 0x10b44100 dir 的地址是 0x10b44100 准备删除文件夹 c 准备删除文件夹 c 准备删除文件夹 c
所以 dir 的地址都是一样的。

接下来验证结论 2 ,这里要稍微有点变化,根据结论 2 可以推断:如果 dir 地址被存放进了匿名函数的内部,后续在匿名函数集 rmdirs 进行循环执行之前,我们去改变这个 dir 临时变量中存放的值(例如把 dir 内部存放的值改为 a ),这样一来应该就是删除 a 文件夹了。
那么应该如何去改变这个 dir 临时变量的值呢?dir 变量的作用域只作用在 range 这一块中,出了 range 之后,其他地方是访问不了的。
哈哈,你想到了,我们可以借助一个外部变量指针来做这件事,改写例程如下:

package main

import (
    "fmt"
    "os"
)

//建立三个临时文件夹
func tempDirs() []string {
    return []string{"a", "b", "c"}
}

func main() {

    //以队列的方式来存放删除操作
    var rmdirs []func()
    var globalDir *string
    for _, dir := range tempDirs() {
        fmt.Println("dir 的地址是 ", &dir)
        //我们从这里获取 dir 的地址,存放到globalDir中
        globalDir = &dir
        os.MkdirAll(dir, 0755)
        rmdirs = append(rmdirs, func() {
            fmt.Println("准备删除文件夹", dir)
            os.RemoveAll(dir)
        })
    }

    //做点别的事情:这里我们把要删除的文件夹改成 a,
    //也就是说如下操作会把 dir 临时变量的值改写成 a
    *globalDir = "a"

    for _, rm := range rmdirs {
        rm()
    }

}

运行之后,得到如下输出:

dir 的地址是  0x10a84100
dir 的地址是  0x10a84100
dir 的地址是  0x10a84100
准备删除文件夹 a
准备删除文件夹 a
准备删除文件夹 a

所以第 2 条结论也得到了证实。

解决

问题原因也找到了,那么如何解决呢,其实我们可以在range 内部加入临时变量解决这个问题,只需要一句就可以搞定:

package main

import (
    "fmt"
    "os"
)

//建立三个临时文件夹 a b c
func tempDirs() []string {
    return []string{"a", "b", "c"}
}

func main() {

    //以队列的方式来存放删除操作
    var rmdirs []func()

    for _, dir := range tempDirs() {
      //将循环体内部的dir 再次赋值给一个新变量
        //此时dir的地址就变化了,不信自己打印试试
      dir := dir
        os.MkdirAll(dir, 0755)
        rmdirs = append(rmdirs, func() {
            fmt.Println("准备删除文件夹", dir)
            os.RemoveAll(dir)
        })
    }

    //做点别的事情

    for _, rm := range rmdirs {
        rm()
    }

}

以上就是全部踩坑小记录了,希望能对你有所帮助。Happy Coding!

本文章首发在 GolangCaff