Closures and Goroutines

Closure is a life saver when it comes to goroutine, but there’s a drawback. Let’s take a look at the following code:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {

	launchGoroutines(10)
	wg.Wait()
	fmt.Println("Main Terminated.")

}

func launchGoroutines(g int){
	for i := 0; i < g; i++{
		wg.Add(1)
		go func(){
			fmt.Println("I am goroutine", i)
			wg.Done()
		}()
		
	}
}

Explanation

10 goroutines were launched. The for loop will break when i := 10. So, the output will have number 0 to 9(not 10). That’s the expected result. Let’s run it:

MacBook-Pro:Downloads kavish$ go run golang.go
I am goroutine 10
I am goroutine 3
I am goroutine 10
I am goroutine 10
I am goroutine 10
I am goroutine 10
I am goroutine 10
I am goroutine 10
I am goroutine 10
I am goroutine 10

Main Terminated

The code has a data race. Being aware of this issue is very important. In a for loop, we have 3 things that takes place:

  • a condition: i < g
  • an init: i := 0
  • and a post statement: i++

As stated in Go Spec: Variables declared by the init statement are re-used in each iteration.

So, i stays the same, and only the value gets changed. That’s why all the goroutines are capturing the same value(10), because they still hold a reference to the same variable(i).

main() and go func() are running in real-time. In what order the goroutines run, is unpredictable. Whether the goroutine starts at launch or in the middle of the loop, they’re still referencing to the same i. The condition says i < 10. i should get the value of 10 to break/stop the loop. Some of the goroutines were still running, even though the loop was terminated, i has already got the value of 10, and that’s the value available for the goroutine to use.

In simple words, goroutines don’t see the value from when they started but the current value. I hope this makes sense.

Find the problem with ‘go vet’ and ‘go -race’

go vet will only report issues that it finds suspicious. It’s useful in some cases. Let’s see:

MacBook-Pro:Downloads kavish$ go vet golang.go
# command-line-arguments
./golang.go:24:34: loop variable i captured by func literal

The above error is common, when using anonymous functions or closures inside a loop. As stated in go vet: it uses heuristics that do not guarantee all reports are genuine problems, but it can find errors not caught by the compilers. You can run it from time to time, it won’t hurt.

go -race will find race conditions in your code. It tells the compiler to monitor all memory addresses, and record when and how memory was accessed. You read about it here. Let’s run it with race detector:

MacBook-Pro:Downloads kavish$ go run -race golang.go
==================
WARNING: DATA RACE
Read at 0x00c0001a6008 by goroutine 7:
  main.launchGoroutines.func1()
      /Users/kavish/Downloads/golang.go:24 +0x3c

Previous write at 0x00c0001a6008 by main goroutine:
  main.launchGoroutines()
      /Users/kavish/Downloads/golang.go:21 +0xc4
  main.main()
      /Users/kavish/Downloads/golang.go:12 +0x3b

Goroutine 7 (running) created at:
  main.launchGoroutines()
      /Users/kavish/Downloads/golang.go:23 +0x9c
  main.main()
      /Users/kavish/Downloads/golang.go:12 +0x3b
==================
I am goroutine 2
==================
WARNING: DATA RACE
Read at 0x00c0001a6008 by goroutine 8:
  main.launchGoroutines.func1()
      /Users/kavish/Downloads/golang.go:24 +0x3c

Previous write at 0x00c0001a6008 by main goroutine:
  main.launchGoroutines()
      /Users/kavish/Downloads/golang.go:21 +0xc4
  main.main()
      /Users/kavish/Downloads/golang.go:12 +0x3b

Goroutine 8 (running) created at:
  main.launchGoroutines()
      /Users/kavish/Downloads/golang.go:23 +0x9c
  main.main()
      /Users/kavish/Downloads/golang.go:12 +0x3b
==================
I am goroutine 2
I am goroutine 3
I am goroutine 4
I am goroutine 5
I am goroutine 5
I am goroutine 7
I am goroutine 8
I am goroutine 9
I am goroutine 10

Main Terminated

Found 2 data race(s)
exit status 66

It shows the problem on line 24(fmt.Println("I am goroutine", i)). As stated in the documentation: It will not issue false positives, so take its warnings seriously.

Fix the issue

Pass i to the closure as a new variable upon each iteration:

func launchGoroutines(g int){
	for i := 0; i < g; i++{
		wg.Add(1)
		go func(fix int){ // declare type of parameter
			fmt.Println("I am goroutine", fix)
			wg.Done()
		}(i) // passing `i` as an argument
		
	}
}

Now, let’s run the code with -race enabled:

MacBook-Pro:Downloads kavish$ go run -race golang.go
I am goroutine 0
I am goroutine 1
I am goroutine 2
I am goroutine 3
I am goroutine 4
I am goroutine 5
I am goroutine 6
I am goroutine 7
I am goroutine 8
I am goroutine 9

Main Terminated

Great. No errors.

“If you cannot do great things, do small things in a great way.”Napoleon Hill