range 踩坑小記——為啥刪不掉資料夾?

wilson_yang發表於2018-06-10

問題

最近在學 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!

每天進步一點點

相關文章