Subscribe to receive notifications of new posts:

A Go Gotcha: When Closures and Goroutines Collide

2015-03-25

1 min read

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.

Cloudflare's connectivity cloud protects entire corporate networks, helps customers build Internet-scale applications efficiently, accelerates any website or Internet application, wards off DDoS attacks, keeps hackers at bay, and can help you on your journey to Zero Trust.

Visit 1.1.1.1 from any device to get started with our free app that makes your Internet faster and safer.

To learn more about our mission to help build a better Internet, start here. If you're looking for a new career direction, check out our open positions.
GoProgrammingBest Practices

Follow on X

Cloudflare|@cloudflare

Related posts

October 09, 2024 1:00 PM

Improving platform resilience at Cloudflare through automation

We realized that we need a way to automatically heal our platform from an operations perspective, and designed and built a workflow orchestration platform to provide these self-healing capabilities across our global network. We explore how this has helped us to reduce the impact on our customers due to operational issues, and the rich variety of similar problems it has empowered us to solve....