問題
最近在學 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 呢?
原因
這裡不賣關子了,直接貼原因出來:
- 這裡就是因為 range 迴圈的時候,只是拷貝了迴圈物件中的元素值出來,放到了臨時變數當中,這個臨時變數的地址是不變的,迴圈結束的時候,該 dir 臨時變數存放的就是 c。
- 然後在我們 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!