Go for的三重陷阱

说明

这篇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)  // 将循环变量作为实参传入
    }
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值