/ website / blog

goroutine closure rule

February 18, 2020

One of the most insidious gotcha’s when coding in Go is the behavior of variable scoping in the context of goroutine closures. By now this foot-gun is well established. Consider the snippet …

for i := 0; i < 10; i++ {
	go func() {
		fmt.Println(i)
	}()
}

On first glance this should print the values [0,10), though perhaps in random order since the print statements are being launched in goroutines. However, the real output is not determinable. Why? Because the variable i inside the launched closure is not copied - it’s the same i that is being manipulated in the goroutine that the for-loop is executing in. Notice go vet captures this bug …

$ ./test.go:10:26: loop variable i captured by func literal

To get around the problem, each closure needs its own copy of i that has been created before the launching of the closure’s goroutine. Often, a naive approach will create a blunt copy directly in the block of the for-loop. This creates some shadowing and visual oddity …

for i := 0; i < 10; i++ {
	i := i // icky
	go func() {
		// our copy of i is safe in here
		fmt.Println(i)
	}()
	// but what if we also reference i here?
}

Instead, we can make use of the closure’s parameters to create a copy of i. This will make sure the i in the launched goroutine is a copy that is safe to reference and modify, while also avoiding the code smell of directly shadowing a variable for the remainder of the loop’s block …

for i := 0; i < 10; i++ {
	go func(i int) {
		fmt.Println(i)
	}(i)
}

goroutine closures may only reference their own parameters is a rule of thumb that has served well over the years. In recent Go versions the go vet tool (which is now also launched automatically as part of go test) will do a pretty good job of catching this mistake.

As with any rule, there are exceptions. The exception to this rule is that any thread-safe type does not need to have a copy made for the closure. Typically this will be in the form of channels that are used for communicating / coordinating with the launched goroutines. For example, this is safe …

	go func() {
		select {
		case i := <-myCh:
			fmt.Println(i)
		}
	}()

However, there may still be some benefit to passing the channel through the parameters of the closure. By doing so, we have an opportunity to make the type of the channel to be read-only or write-only, if we know the closure is only to be used for one of those operations …

	go func(myCh <-chan int) {
		select {
		case i := <-myCh:
			fmt.Println(i)
		}
	}(myCh)

This goes a long way toward describing the intent of the channel, and can prevent some of those “am I holding it wrong” bugs that pop up several years and maintainers down the line …

	go func(myCh <-chan int) {
		select {
		case i := <-myCh:
			fmt.Println(i)
		}
		myCh <- 42 // compile error
	}(myCh)

gl;hf!

➡️ related posts in the go-code series ...