Closures and Goroutines
Table of Contents
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