3 ms·
The error in the Go snippet is that "v" is a single memory location shared throughout the loop. The closure doesn't get a copy, so what usually ends up happeni
by lobster_johnson 10y ago
The error in the Go snippet is that "v" is a single memory location shared throughout the loop.
The closure doesn't get a copy, so what usually ends up happening is that every goroutine gets the last value (since the goroutines are probably not scheduled until the end of the loop).
This problem doesn't just affect loops, of course: A goroutine can access any local environment outside its scope, and local variables can mutate.
The worst surprise I ever encountered was this (simplified):
func makeWorkersDocomplicatedStuff()
ch := make(chan string)
defer func() {
if ch != nil {
close(ch)
}
}
for i := 0; i < numWorkers; i++ {
go func() {
for {
select {
case s := <-ch:
// ...
}
}
}
}
close(ch)
ch = nil
// ... More stuff ...
}
What will happen here is that the goroutines will all block forever, because "ch" becomes nil, and in Go, polling a nil channel will block forever (it's an interesting design choice in such a strict language).
Rewriting and refactoring this was trivial enough, but catching it was time wasted. Lessons learned: (1) Be scrupulous about closure environments, (2) be super careful about nil channels, and (3) try to avoid defers whose concerns don't fully encapsulate the function body (so defers in the middle of a function is often code smell).
- tatterdemalion 10y ago> The error in the Go snippet is that "v" is a single memory location shared throughout the loop. In Rust this is not true. However, the same concept applies - say it was a variable from outside the loop. In that case, you would get a clear compile time error about moving the value into the thread's closure more than once. A similar error would apply if you iterated over a container by reference (instead of by value).