A Go Gotcha: When Closures and Goroutines Collide

by John Graham-Cumming.

Here's a small Go gotcha that it's easy to fall into when using goroutines and closures. Here's a simple program that prints out the numbers 0 to 9:

(You can play with this in the Go Playground here)

package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		fmt.Printf("%d ", i)
	}
}

It's output is easy to predict:

0 1 2 3 4 5 6 7 8 9

If you decided that it would be nice to run those fmt.Printfs concurrently using goroutines you might be surprised by the result. Here's a version of the code that runs each fmt.Printf in its own goroutine and uses a sync.WaitGroup to wait for the goroutines to terminate.

package main

import (
	"fmt"
    "runtime"
	"sync"
)

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU())
    
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			fmt.Printf("%d ", i)
			wg.Done()
		}()
	}
	
	wg.Wait()
}

(This code is in the Go Playground here). If you're thinking concurrently then you'll likely predict that the output will be the numbers 0 to 9 in some random order depending on precisely when the 10 goroutines run.

But the output is actually:

10 10 10 10 10 10 10 10 10 10

Why?

Because each of those goroutines is sharing the single variable i across the ten closures generated by the func() used for each goroutine.

The output from the goroutines will depend on the value of i when they start running. In the example, above they didn't actually start running until the loop had terminated and i had the value 10.

This programmer error can have other weird effects depending on the variable that's being shared across the goroutine closures.

To solve this the simplest solution is to create a new variable, a parameter to the func() and pass i into the function call. Like this:

package main

import (
	"fmt"
    "runtime"
	"sync"
)

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU())
    
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			fmt.Printf("%d ", i)
			wg.Done()
		}(i)
	}
	
	wg.Wait()
}

(The code for that is here). That works correctly.

This is such a common gotcha that it's also in the FAQ.

comments powered by Disqus