Even with Go’s built-in support for concurrency, bugs still happen. It’s easy to accidentally reference a variable from two different goroutines without acquiring a lock. The computer science term for this is a data race. To help find these sorts of bugs, Go includes a race detector. It isn’t guaranteed to find every single data race in your code, but if it finds one, you should put proper locks around what it finds.
Look at a simple example in the file examples/race/race.go:
func getCounter() int {
var counter int
var wg sync.WaitGroup
for range 5 {
wg.Go(func() {
for range 1000 {
counter++
}
})
}
wg.Wait()
return counter
}
This code launches five goroutines, has each of them update a shared counter variable 1,000 times, and then returns the result. You’d expect it to be 5,000, so let’s verify this with a unit test in examples/race/race_test.go:
func TestGetCounter(t *testing.T) {
counter := getCounter()
if counter != 5000 {
t.Error("unexpected counter:", counter)
}
}
If you run go test a few times, you’ll see that sometimes it passes, but most of the time it fails with an error message like this:
unexpected counter: 3673
The problem is that there’s a data race in the code. In a program this simple, the cause is obvious: multiple goroutines are trying to update counter simultaneously, and some of their updates are lost. In more complicated programs, these sorts of races are harder to see. Let’s see what the data race detector does. Use the flag -race with go test to enable it:
$ go test -race
==================
WARNING: DATA RACE
Read at 0x00c000128070 by goroutine 10:
test_examples/race.getCounter.func1()
test_examples/race/race.go:12 +0x45
Previous write at 0x00c000128070 by goroutine 8:
test_examples/race.getCounter.func1()
test_examples/race/race.go:12 +0x5b
The traces make it clear that the line counter++ is the source of your problems.
Warning
Some people try to fix race conditions by inserting “sleeps” into their code, trying to space out access to the variable that’s being accessed by multiple goroutines. This is a bad idea. Doing so might eliminate the problem in some cases, but the code is still wrong and will fail in some situations.
You can also use the -race flag when you build your programs. This creates a binary that includes the data race detector and that reports any races it finds to the console. This allows you to find data races in code that doesn’t have tests.
If the data race detector is so useful, why isn’t it enabled all the time for testing and production? A binary with -race enabled runs approximately 10 times slower than a normal binary. That isn’t a problem for test suites that take a second to run, but for large test suites that take several minutes, a 10× slowdown reduces productivity.
For more information on the data race detector, check out its official documentation.