在每个循环中使用Go例程时,由func literal捕获的范围变量



我不太确定'func literal'是什么,因此这个错误让我有点困惑。我想我看到了这个问题-我从一个新的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: %+vn %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: %+vn %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
} 

但这真的能解决问题吗?我只是将引用从range变量移动到另一个局部变量,该局部变量的值基于我所处的每个循环的迭代。

不要感到难过,这是Go新手的常见错误,是的,var currentProcess在每个循环中都会更改,因此您的例程将使用切片 1中的最后一个进程。处理,您所要做的就是将变量作为参数传递给匿名函数,如下所示:

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: %+vn %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
}

寻找更简单的例子:

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命令,它会发出警告。Go vet有助于精确地找到这种可疑的结构,它使用启发式方法,不能保证所有报告都是真正的问题,但它可以找到编译器未捕获的错误。因此,不时地运行它是一个很好的实践。

Go Playground在运行代码之前运行 Go vet,您可以在这里看到它的作用。

正确:

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)
}

我有意将函数字面值参数命名为differentI,以使它明显是一个不同的变量。这样做对于并发使用是安全的,go/strong>不会抱怨,你也不会得到奇怪的行为。你可以在这里看到它的作用。(由于打印是在不同的go例程中完成的,因此您将看不到任何内容,但程序将成功退出)

顺便说一下,func literal基本上是一个匿名函数:)

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

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

最新更新