【问题标题】:go vet range variable captured by func literal when using go routine inside of for each loop在 for 每个循环中使用 go 例程时,由 func 文字捕获的 go vet 范围变量
【发布时间】:2017-03-12 14:58:15
【问题描述】:

我不太确定“func 文字”是什么,因此这个错误让我有点困惑。我想我看到了这个问题——我从一个新的 go 例程中引用一个范围值变量,因此该值可能随时改变,而不是我们所期望的。解决问题的最佳方法是什么?

有问题的代码:

func (l *Loader) StartAsynchronous() []LoaderProcess {
    for _, currentProcess := range l.processes {
        cmd := exec.Command(currentProcess.Command, currentProcess.Arguments...)
        log.LogMessage("Asynchronously executing LoaderProcess: %+v", currentProcess)
        go func() {
            output, err := cmd.CombinedOutput()
            if err != nil {
                log.LogMessage("LoaderProcess exited with error status: %+v\n %v", currentProcess, err.Error())
            } else {
                log.LogMessage("LoaderProcess exited successfully: %+v", currentProcess)
                currentProcess.Log.LogMessage(string(output))
            }
            time.Sleep(time.Second * TIME_BETWEEN_SUCCESSIVE_ITERATIONS)
        }()
    }
    return l.processes
}

我建议的解决方法:

func (l *Loader) StartAsynchronous() []LoaderProcess {
    for _, currentProcess := range l.processes {
        cmd := exec.Command(currentProcess.Command, currentProcess.Arguments...)
        log.LogMessage("Asynchronously executing LoaderProcess: %+v", currentProcess)
        localProcess := currentProcess
        go func() {
            output, err := cmd.CombinedOutput()
            if err != nil {
                log.LogMessage("LoaderProcess exited with error status: %+v\n %v", localProcess, err.Error())
            } else {
                log.LogMessage("LoaderProcess exited successfully: %+v", localProcess)
                localProcess.Log.LogMessage(string(output))
            }
            time.Sleep(time.Second * TIME_BETWEEN_SUCCESSIVE_ITERATIONS)
        }()
    }
    return l.processes
} 

但这真的能解决问题吗?我刚刚将引用从范围变量移动到另一个局部变量,其值基于我所在的每个循环的迭代。

【问题讨论】:

标签: go


【解决方案1】:

不要觉得这对 Go 新手来说是一个常见的错误,是的,每个循环的 var currentProcess 都会发生变化,因此您的 goroutine 将使用切片中的最后一个进程 l .processes,你所要做的就是将变量作为参数传递给匿名函数,如下所示:

func (l *Loader) StartAsynchronous() []LoaderProcess {

    for ix := range l.processes {

        go func(currentProcess *LoaderProcess) {

            cmd := exec.Command(currentProcess.Command, currentProcess.Arguments...)
            log.LogMessage("Asynchronously executing LoaderProcess: %+v", currentProcess)

            output, err := cmd.CombinedOutput()
            if err != nil {
                log.LogMessage("LoaderProcess exited with error status: %+v\n %v", currentProcess, err.Error())
            } else {
                log.LogMessage("LoaderProcess exited successfully: %+v", currentProcess)
                currentProcess.Log.LogMessage(string(output))
            }

            time.Sleep(time.Second * TIME_BETWEEN_SUCCESSIVE_ITERATIONS)

        }(&l.processes[ix]) // passing the current process using index

    }

    return l.processes
}

【讨论】:

  • 感谢更新的代码看起来很棒!我的代码中有一个棘手的错误,需要一个多小时才能修复与非常相似的东西相关的问题。我在返回 []*LoaderProcess 之前没有意识到切片已经是一个指针,所以我实际上是在返回一个指针切片,其中每个指针都指向同一个 LoaderProcess 实例,该实例恰好是最后一个要完成的命令。每次执行后都不一样。因此,从一段代码中吸取了两大教训。谢谢。
  • 我们必须使用索引发送吗?
【解决方案2】:

对于那些寻找更简单示例的人:

这是错误的:

func main() {
  for i:=0; i<10; i++{
    go func(){

        // Here i is a "free" variable, since it wasn't declared
        // as an explicit parameter of the func literal, 
        // so IT'S NOT copied by value as one may infer. Instead,
        // the "current" value of i
        // (in most cases the last value of the loop) is used
        // in all the go routines once they are executed.

        processValue(i)

    }()
  }
}

func processValue(i int){
  fmt.Println(i)
}

不完全是错误,但可能导致意外行为,因为控制循环的变量 i 可能会从其他 go 例程更改。实际上是 go vet command 对此发出警报。 Go vet 有助于准确地发现这种可疑结构,它使用不能保证所有报告都是真实问题的启发式方法,但它可以找到编译器未捕获的错误。因此,不时运行它是一个好习惯。

Go Playground 在运行代码之前运行 go vet,您可以在运行中看到 here

这是正确的:

func main() {
  for i:=0; i<10; i++{
    go func(differentI int){

        processValue(differentI)

    }(i) // Here i is effectively passed by value since it was
         // declared as an explicit parameter of the func literal
         // and is taken as a different "differentI" for each
         // go routine, no matter when the go routine is executed
         // and independently of the current value of i.
  }
}

func processValue(i int){
  fmt.Println(i)
}

我故意将 func 文字参数命名为 differentI 以表明它是一个不同的变量。这样做对于并发使用是安全的,go vet 不会抱怨,也不会出现奇怪的行为。您可以在here 中看到这一点。 (你什么都看不到,因为打印是在不同的 goroutine 上完成的,但程序会成功退出)

顺便说一句,func 文字基本上是一个匿名函数:)

【讨论】:

  • 我有点明白为什么最初的例子是错误的,但直觉上,它并不完全适合我。在那种情况下,变量 i 不只是一个整数而不是一个指针,那么现实,它不应该是一个错误吗?
  • 围绕这个主题的 go internals 非常有趣,看看this。当你不在闭包内声明显式参数时,go 编译器必须将它们视为指针,因此它们最终使用相同的 i 值。值得全面了解这些东西,感谢您的观察。希望对您有所帮助。
【解决方案3】:

是的,您所做的是正确修复此警告的最简单方法。

在修复之前,只有一个 single 变量,所有的 goroutine 都在引用它。这意味着他们看到的不是开始时的值,而是当前值。在大多数情况下,这是该范围内的最后一个。

【讨论】:

  • 太棒了,谢谢。你能确认 func 文字只是一个内联定义的函数吗?例如内联 go 例程所要求的?
【解决方案4】:

如果您在 JS 中使用过 Javascript 和闭包,那么将其与 for 循环内的 setTimeout 的经典示例与使用迭代结束时循环 var 的最终值的 setTimeout 非常相似且易于比较.

【讨论】:

    猜你喜欢
    • 2016-10-31
    • 2018-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-30
    • 2015-04-24
    • 1970-01-01
    • 2014-03-31
    相关资源
    最近更新 更多