Did you ever run your Go code, and you felt like something was off? Somehow, you had the feeling that variables were not correctly used? And that this might be connected to the Goroutine? Then, you might be onto something!
Recently, I have been learning how to write Go code and I wrote something like this:
func TestSuspectedCodeSmell(t *testing.T) {
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)
}()
}
}
The above code might print something along those lines (can change every run):
=== RUN TestSuspectedCodeSmell
5
5
5
5
5
--- PASS: TestSuspectedCodeSmell (0.00s)
And, you might be wondering, how is it possible that the Goroutines all print the same number and even one that is outside the condition?
Well, this is because variable i
is not saved into scope of the Goroutine, rather it uses the outer scope. This leads to some pretty weird errors.
To fix this, pass the variable as a parameter to the Goroutine!
An improved version of the code would be:
func TestCodeSmellFixed(t *testing.T) {
for i := 0; i < 5; i++ {
go func(val int) {
fmt.Println(val)
}(i)
}
}
Which could print something like this:
=== RUN TestCodeSmellFixed
4
2
3
1
0
--- PASS: TestCodeSmellFixed (0.00s)
By the way, I did omit the necessary wait group code to improve the readability. Without it, the test just runs through without completing all Goroutines. Here is the full code:
package main
import (
"fmt"
"sync"
"testing"
)
func TestSuspectedCodeSmell(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i)
}()
}
wg.Wait()
}
func TestCodeSmellFixed(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
fmt.Println(val)
}(i)
}
wg.Wait()
}