← Back to Go Course | Chapter 11: Concurrency | Lesson 7 of 8

Race Conditions

A race condition happens when two goroutines rush to touch the same piece of data at the same time, and whoever wins changes the outcome unpredictably.
Syntax
go
var mu sync.Mutex
var shared int

mu.Lock()
shared++          // access shared data only while locked
mu.Unlock()

// detect races: go run -race main.go

What a Race Condition Looks Like

When two or more goroutines read and write the same variable without any coordination, the final result depends on unpredictable timing -- the same program can produce different, sometimes wrong, results across separate runs.

Example: What a Race Condition Looks Like

markup
package main

import (
	"fmt"
	"sync"
)

func main() {
	total := 0
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			total++ // unsynchronized: a real race in production code
		}()
	}
	wg.Wait()
	fmt.Println("total (may vary if truly racy):", total)
}

Detecting Races with -race

Go ships a race detector, enabled with 'go run -race' or 'go test -race', which instruments memory accesses and reports the exact goroutines and lines involved when it observes a genuine race, even if the output happened to look correct that run.

Note: Always run tests with -race in CI when the code involves goroutines sharing data.

Example: Detecting Races with -race

bash
go run -race main.go

⚠️ Run this in your own terminal or Node.js environment.

Fixing a Race with a Mutex

The standard fix for a race on shared data is to protect every access to it with a mutex, ensuring only one goroutine can read or write the variable at any given instant.

Example: Fixing a Race with a Mutex

markup
// Every Go file belongs to a package; main builds an executable
package main

// Import the packages this program uses
import (
	"fmt"
	"sync"
)

// main is where the program starts running
func main() {
	// Declare mu with the zero value of its type
	var mu sync.Mutex
	// Short declaration: create total and infer the type
	total := 0
	// Declare wg with the zero value of its type
	var wg sync.WaitGroup
	// Loop: repeat while the condition holds
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		// Start this call in a new goroutine (runs concurrently)
		go func() {
			defer wg.Done()
			mu.Lock()
			total++
			mu.Unlock()
		}()
	}
	wg.Wait()
	fmt.Println("total:", total)
}
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
Common Mistakes
  1. Assuming a program that 'usually works' during testing has no race condition -- races are timing-dependent and can hide until production load exposes them.
  2. Reading a shared variable from one goroutine while another writes it, without a mutex or channel, believing simple reads are always safe.
  3. Never running 'go test -race' or 'go run -race', missing races that only the race detector reliably surfaces.
Chapter Summary
  • A race condition occurs when multiple goroutines access shared data concurrently and at least one writes it, without synchronization.
  • Races produce unpredictable results that can vary between runs, making them hard to debug by inspection alone.
  • Go's built-in race detector (-race flag) instruments a program to catch races at runtime.
  • Fixing a race means synchronizing access with a mutex, channel, or avoiding the shared mutable state entirely.
🔒

Chapter Quiz — Complete all 8 topics to unlock

0/8 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.