Skip to Content
Course content

36: Detecting Race Conditions with the Race Detector

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once spent three days chasing a bug in a production service that only happened on Tuesdays. Not every Tuesday—just when traffic spiked. The logs were clean, the logic looked sound, but the final counts were always slightly off. It turns out I had a classic race condition. The terrifying thing about race conditions is that they are non-deterministic. Your code might work perfectly on your laptop, but the second it hits a multi-core server under load, it falls apart.

The bug that doesn't look like a bug

Let's look at a piece of code I've put together. Imagine we're building a simple hit counter for a website. We want to track how many times a page is visited across multiple concurrent requests.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var count int
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			count++ // This looks innocent, right?
		}()
	}

	wg.Wait()
	fmt.Printf("Final count: %d\n", count)
}

If I run this with go run main.go, I might see Final count: 1000. I might see 942. I might see 811. If I'm lucky (or unlucky), it works perfectly the first five times I run it, and I tell myself the code is fine. But count++ isn't an atomic operation; it's a read, an increment, and a write. When two goroutines do this at the exact same time, they both read the value 10, both increment it to 11, and both write 11 back. We just lost a visit.

Turning on the X-ray

Since we can't always rely on the bug manifesting during a standard run, we use the Race Detector. It's built directly into the Go toolchain. I don't have to install anything extra; I just add a flag to my command.

Let's try running that same program again, but this time with the -race flag:

go run -race main.go

Immediately, the output changes. Instead of a simple number, I get a wall of text that looks like a crash report. It tells me exactly what happened: WARNING: DATA RACE. It points to the specific line where the write occurred and the specific line where another goroutine was reading or writing to that same memory address.

I love this output because it doesn't just say "something is wrong." It gives me a stack trace for both competing goroutines. It's essentially the tool saying, "Hey, Goroutine A was trying to change this variable at the same time Goroutine B was touching it. You've got a problem here."

Cleaning up the mess

Now that the race detector has pointed the finger, I can't ignore it. To fix this, I need to ensure that only one goroutine can touch count at a time. A sync.Mutex is the most straightforward tool for the job here.

I'll adjust the code to lock the resource before the increment and unlock it immediately after.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var count int
	var wg sync.WaitGroup
	var mu sync.Mutex // Added a mutex to guard the count

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			
			mu.Lock()
			count++ 
			mu.Unlock()
		}()
	}

	wg.Wait()
	fmt.Printf("Final count: %d\n", count)
}

Now, I run it again with go run -race main.go. This time, the output is clean. No warnings, and the count is consistently 1000. The race detector is silent, which in this case, is exactly what we want.

A quick word of caution

You might be tempted to leave the -race flag on in your production environment. Don't. The race detector works by instrumenting your code, which adds significant overhead—usually increasing memory usage and slowing down execution by 2x to 20x. It's a powerhouse for testing and CI/CD pipelines, but it's not meant for the live environment.




📋 Practical Task

Fixing the Concurrent Map Crash

In Go, maps are not safe for concurrent use. If two goroutines write to a map at the same time, the runtime will actually panic and crash the program. However, sometimes you have a "read-write" race that doesn't cause an immediate panic but results in corrupted data.

Create a program where multiple goroutines are updating a shared map of "user scores". For example, have 10 goroutines each incrementing the score of a specific user 100 times.

Your requirements:

  • Create a map[string]int to hold scores.
  • Launch several goroutines that update the map concurrently.
  • Run the program with go run -race and observe the data race warning.
  • Implement a sync.Mutex to synchronize access to the map.
  • Verify that the race detector no longer reports any issues.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.