Concurrency practices Spawning Go routine closures in a loop
This article explores spawning goroutines in Go within a loop, highlighting closure variable capture and techniques to ensure correct goroutine behavior.
In this article, we explore a common concurrency pattern in Go: spawning goroutines from within a closure inside a loop. We’ll explain how closures capture variables from their surrounding scope, highlight potential pitfalls with this approach, and demonstrate the correct technique to ensure each goroutine works with the intended value.
A closure in Go is a function defined within another function that has access to the parent function’s local variables. Consider the following example:
Copy
Ask AI
func main() { var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { fmt.Println(i) wg.Done() }() } fmt.Println("Done.") wg.Wait()}
In this snippet, a WaitGroup is used to synchronize 10 goroutines. The loop runs from 0 to 9 and spawns a new goroutine on each iteration. Each goroutine captures the loop variable i and prints its value before calling wg.Done().
Although this code appears straightforward, the goroutines capture the variable i from the outer scope. Because goroutines may start executing after the loop has advanced or finished, they can all see the same (possibly final) value of i.
Below is the complete program with all necessary imports and a proper structure:
Copy
Ask AI
package mainimport ( "fmt" "sync")func main() { var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { fmt.Println(i) wg.Done() }() } wg.Wait() fmt.Println("Done.")}
When executed, you might expect the output to contain the numbers 0 through 9 (in any order) followed by the “Done.” message. However, due to the nature of closures and asynchronous execution, the output may become non-deterministic, showing unexpected values.
The core issue is that goroutines do not begin execution immediately. They are scheduled concurrently, meaning by the time a goroutine runs, the loop variable i might have already been incremented (or the loop might have ended), causing all goroutines to print an unexpected value (for example, all printing 10).
To ensure that each goroutine captures the intended value of i at the moment it is spawned, the common solution is to pass i as an argument to the closure. This approach ensures that each goroutine has its own copy of the variable with the correct value.Below is the revised version of the program:
Copy
Ask AI
package mainimport ( "fmt" "sync")func main() { var wg sync.WaitGroup wg.Add(10) for i := 1; i <= 10; i++ { go func(i int) { fmt.Println(i) wg.Done() }(i) } wg.Wait() fmt.Println("Done.")}
In this corrected example, passing i as an argument to the anonymous function guarantees that each goroutine works with its own copy of the value. Running this program should correctly print the numbers 1 through 10 in any order, followed by “Done.”.
When spawning goroutines within a loop in Go, it’s crucial to be aware of how closures capture variables. To avoid issues that arise from the deferred evaluation of loop variables, always pass the loop variable as an argument to the closure. This technique not only leads to more predictable behavior but also ensures that each goroutine processes the intended value.We hope you found this guide helpful. For more insights and advanced topics on Go concurrency, stay tuned to our future articles.