说明
这篇for的三重陷阱取自我的另一篇博文Go学习笔记。为了方便快速的查找,将其复制了过来。
for陷阱之循环变量
在使用匿名函数时,须特别注意在for词块中引入的循环变量。如果在匿名函数里保存了这个变量,则会造成意想不到的结果,例如,下面的程序试图将一个数组的元素全部临置成0,随后再恢复原来的值:
func main() { a := [3]int{1, 2, 3} var recs []func() for i, v := range a { a[i] = 0 recs = append(recs, func() { a[i] = v // 错误! }) } // do something // 试图恢复原来的值 for _, rec := range recs { rec() } fmt.Println(a) // 打印 [0 0 3] }
最后得到了错误的结果。原因如下:for循环引入了一个新的词块,在这个词块中声明了i和v。所有在循环体里创建的匿名函数都捕捉了这两个变量本身(变量的地址),而不是捕捉了这两个变量的值。因此在试图恢复数组的值时,每个匿名函数都是以第一个for循环结束时i和v的值(2和3)带入。相当于执行了3次a[2]=3。为了解决这个问题,通常的做法是在循环体内部声明一个同名变量作为拷贝,再将这个新声明的变量带入匿名函数。将上面的第一个for循环修改如下:
for i, v := range a { i, v := i, v // 声明同名变量,拷贝i和v的值 a[i] = 0 recs = append(recs, func() { a[i] = v }) }
解决问题的关键点是每个存在于匿名函数体之内的变量,都有其自己的地址。
for陷阱之defer
不要在循环体里用defer清理资源,考虑以下程序:
package main import ( "path/filepath" "os" ) var filenames []string func main() { processDir(`G:\projects`) } func processDir(root string) error { // filepath.Walk(rootDir, walkFn)遍历rootDir下的所有 // 文件和文件夹,每遇到一个文件或文件夹,都会调用walkFn filepath.Walk(root, walkHandler) // 遍历文件路径数组,打开文件,处理文件 for _, filename := range filenames { file, err := os.Open(filename) if err != nil { return err } defer file.Close() // 危险!有可能耗尽内存! // do something with file // ... } return nil } func walkHandler(path string, info os.FileInfo, err error) error { if err != nil { return err } // 如果是文件,则保存文件路径到数组 if !info.IsDir() { filenames = append(filenames, path) } return nil }
defer file.Close()
语句是在函数return
语句执行完毕后才会执行的,不会在for
循环体中执行。for
循环的每次遍历都打开一个文件句柄,但没有在循环体结束前关闭它。随着for
循环迭代次数的增加,有可能会耗尽内存。正确的做法是将for
循环体中的语句放在一个函数中:func processFile(filename string) error { file, err := os.Open(filename) if err != nil { return err } defer file.Close() // ok now // do something with file // ... return nil } func processDir(root string) error { // filepath.Walk(rootDir, walkFn)遍历rootDir下的所有 // 文件和文件夹,每遇到一个文件或文件夹,都会调用walkFn filepath.Walk(root, walkHandler) // 遍历文件路径数组,处理文件 for _, filename := range filenames { processFile(filename) } return nil }
当然,更简单的,也可以用闭包:
func processDir(root string) error { // filepath.Walk(rootDir, walkFn)遍历rootDir下的所有 // 文件和文件夹,每遇到一个文件或文件夹,都会调用walkFn filepath.Walk(root, walkHandler) // 遍历文件路径数组,处理文件 for _, filename := range filenames { err := func () error { file, err := os.Open(filename) if err != nil { return err } defer file.Close() // ok now // do something with file // ... return nil }() if err != nil { return err } } return nil }
for陷阱之go
考虑以下用协程打印0到9的程序:
package main import "fmt" func main() { s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} ch := make(chan struct{}) for _, v := range s { go func() { fmt.Println(v) // 错误 ch <- struct{}{} }() } // 等待所有的协程结束 for range s { <- ch } }
程序的输出并非0到9,而是会有若干重复。这个错误与[for陷阱之循环变量]类似,都是在闭包里捕获了循环变量。在这个例子里,for循环的每一次迭代都在开启了一个协程后马上进入下一次迭代,开启另一个协程,这时上一个协程可能还没来得及运行,但是捕获的循环变量的值却已经改变,导致了多个协程处理同一个值的情况。解决方法是在闭包里避免捕获循环变量,因为Go函数参数都是值传递,因此可以给闭包增加一个参数,并将循环变量作为实参传递:
for _, v := range s { go func(v int) { fmt.Println(v) // ok, v是闭包的形参,值与循环变量一致 ch <- struct{}{} }(v) // 将循环变量作为实参传入 }