Go Memory Model

Learning Objectives

By the end of this tutorial, you will be able to:

  • Master happens-before relationships and understand memory ordering guarantees
  • Prevent data races using proper synchronization primitives
  • Choose the right synchronization tool for your use case
  • Build lock-free data structures using atomic operations safely
  • Debug concurrent issues with systematic approaches and tools
  • Write production-safe concurrent code that works correctly under all conditions

Hook - The Concurrent Programming Challenge

When coordinating a team where multiple people need to read and update the same whiteboard, without clear rules about who can write when and who gets to see what changes, chaos ensues. Go's memory model is exactly that set of rules for concurrent programming—it ensures all goroutines see a consistent view of shared data.

Real-World Impact:

  • Cloudflare's 2019 outage: Memory ordering bug caused incorrect request routing globally for 30 minutes
  • Uber's payment system bug: Race condition allowed double charging customers
  • Financial impact: These issues cost companies millions and damaged customer trust

💡 Key Takeaway: The Go memory model is your contract for safe concurrent programming. It tells you exactly when writes from one goroutine become visible to reads in another goroutine.

Why the Memory Model Matters

Think of the memory model as traffic rules for concurrent programming. Just as traffic rules prevent accidents and ensure smooth flow, memory model rules prevent data races and ensure program correctness.

🎯 Practical Analogy: Imagine a shared kitchen where multiple chefs cook simultaneously. The memory model is like the rule: "Only one chef can use the stove at a time" and "Everyone must see when a dish is finished cooking." Without these rules, you'd have burned food and confused chefs.

The Go memory model specifies the conditions under which reads of a variable in one goroutine are guaranteed to observe values produced by writes to the same variable in another goroutine.

Critical Need for Memory Model Knowledge

  • 🔒 Correctness: Prevent data races and undefined behavior that can corrupt data
  • 🚀 Performance: Use appropriate synchronization—not too much and not too little
  • 🐛 Debugging: Understand why concurrent bugs happen and how to fix them systematically
  • 📊 Optimization: Know when compiler and runtime optimizations are safe to apply

Real-world Example: In a high-frequency trading system, understanding the memory model ensures that price updates from one goroutine are immediately visible to trading decision goroutines, preventing stale data trades worth millions.

Real-World Incidents

Cloudflare's Race Condition:

  • What happened: Memory ordering bug caused incorrect request routing across their global network
  • Impact: 30 minutes of global service degradation affecting millions of users
  • Root cause: Misunderstanding of Go's memory model—assuming writes would be visible without proper synchronization
  • Fix: Implemented proper mutex synchronization around shared routing data
  • Cost: Estimated millions in lost revenue and customer trust

Uber's Payment Bug:

  • What happened: Race condition in payment processing allowed double charging
  • Impact: Money debited twice from customer accounts, some customers charged thousands instead of hundreds
  • Root cause: Concurrent goroutines accessing shared payment state without proper synchronization
  • Fix: Added atomic operations and proper locking around payment state transitions
  • Cost: $250,000 in refunds plus reputational damage

⚠️ Important: These incidents show that data races aren't just theoretical—they can cause real production issues with financial impact.

Fundamentals of the Memory Model

What is a Memory Model

A memory model defines the contract between programmers and the system about memory access ordering in concurrent programs. It specifies:

  1. Visibility: When writes from one thread become visible to reads in another
  2. Ordering: What order operations appear to execute in
  3. Atomicity: Which operations complete as indivisible units
  4. Coherence: How different memory locations relate to each other

💡 Key Concept: Without a memory model, compilers and CPUs can reorder operations in ways that break concurrent programs. The memory model defines what reorderings are allowed and what guarantees you have.

Sequential Consistency vs Weak Memory Models

Sequential Consistency (strongest guarantee):

  • Operations appear to execute in some total order
  • All processors see operations in the same order
  • Easy to reason about but expensive to implement

Weak Memory Models (what Go uses):

  • Allow reordering for performance
  • Provide happens-before relationships instead of total ordering
  • More complex but much faster
 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"runtime"
 7	"sync"
 8	"time"
 9)
10
11// Demonstration of memory ordering effects
12func demonstrateWeakMemory() {
13	var x, y int
14	var a, b int
15
16	iterations := 1000
17	concurrent := 0
18
19	for i := 0; i < iterations; i++ {
20		x, y, a, b = 0, 0, 0, 0
21
22		done := make(chan bool, 2)
23
24		// Goroutine 1: Write x, read y
25		go func() {
26			x = 1
27			a = y
28			done <- true
29		}()
30
31		// Goroutine 2: Write y, read x
32		go func() {
33			y = 1
34			b = x
35			done <- true
36		}()
37
38		<-done
39		<-done
40
41		// In a sequentially consistent model, a=0,b=0 is impossible
42		// But with weak memory ordering, it can happen!
43		if a == 0 && b == 0 {
44			concurrent++
45		}
46	}
47
48	fmt.Printf("Out of %d runs:\n", iterations)
49	fmt.Printf("Concurrent executions (a=0, b=0): %d\n", concurrent)
50	fmt.Printf("This shows weak memory ordering in action\n")
51}
52
53func main() {
54	fmt.Println("=== Memory Ordering Demonstration ===\n")
55	runtime.GOMAXPROCS(4) // Use multiple processors
56	demonstrateWeakMemory()
57}

Memory Barriers and Synchronization

Memory Barriers (or fences) prevent certain types of reordering:

  • Compiler barriers: Prevent compiler reordering
  • Hardware barriers: Prevent CPU reordering
  • Both: Go's synchronization primitives provide both

Synchronization Primitives in Go:

  1. Channels: Send/receive operations
  2. Mutexes: Lock/unlock operations
  3. Atomic operations: Load/store/CAS operations
  4. sync.Once: One-time initialization
  5. sync.WaitGroup: Coordination barriers

Happens-Before Relationship

🎯 Practical Analogy: Think of happens-before relationships like a timeline of events in a story. If Chapter 1 happens-before Chapter 2, then everything established in Chapter 1 is available to readers when they start Chapter 2. In Go, this ensures that all writes from one point are visible to reads at later points.

The Core Concept

A happens-before relationship defines when memory operations are guaranteed to be visible across goroutines. Without these relationships, there are no guarantees about order or visibility.

💡 Key Takeaway: If there's no happens-before relationship between two operations, the order in which they appear to execute is undefined, even if they're in the same program!

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"time"
 7)
 8
 9var a, b int
10
11// WITHOUT synchronization
12func wrong() {
13	// Goroutine 1
14	go func() {
15		a = 1 // Write 1
16		b = 2 // Write 2
17	}()
18
19	// Goroutine 2
20	go func() {
21		for b == 0 { // Wait for b
22			// Busy wait
23		}
24		// When we get here, is a guaranteed to be 1?
25		// NO! There's no happens-before relationship
26		fmt.Println(a) // Could print 0 or 1
27	}()
28
29	time.Sleep(100 * time.Millisecond)
30}
31
32func main() {
33	wrong()
34}

Correct Synchronization

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9var a, b int
10var mu sync.Mutex
11
12// WITH synchronization
13func correct() {
14	var wg sync.WaitGroup
15	wg.Add(2)
16
17	// Goroutine 1
18	go func() {
19		defer wg.Done()
20		mu.Lock()
21		a = 1
22		b = 2
23		mu.Unlock()
24	}()
25
26	// Goroutine 2
27	go func() {
28		defer wg.Done()
29		mu.Lock()
30		val := b
31		mu.Unlock()
32
33		if val != 0 {
34			mu.Lock()
35			fmt.Println("a =", a) // Guaranteed to be 1
36			mu.Unlock()
37		}
38	}()
39
40	wg.Wait()
41}
42
43func main() {
44	correct()
45}

Formal Happens-Before Rules

The Go memory model defines several happens-before relationships:

Rule 1: Within a single goroutine, operations execute in program order

If statement A appears before statement B in the source code,
then A happens-before B

Rule 2: Send on a channel happens-before receive completes

ch <- value  // Send
...
<-ch         // Receive sees the value

Rule 3: Receive from an unbuffered channel happens-before send completes

<-ch         // Receive
...
ch <- value  // Send can complete

Rule 4: Unlock happens-before any subsequent lock

mu.Lock()
// Critical section 1
mu.Unlock()  // Unlock happens-before next Lock

mu.Lock()
// Critical section 2: sees effects from critical section 1
mu.Unlock()

Rule 5: Read from atomic variable observes any prior write

atomic.Store(&x, 42)  // Write happens-before read
v := atomic.Load(&x)  // Sees 42
 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9func demonstrateHappensBefore() {
10	var x, y int
11	var wg sync.WaitGroup
12
13	// Rule 1: Within a single goroutine, happens-before order is program order
14	wg.Add(1)
15	go func() {
16		defer wg.Done()
17		x = 1  // Statement A
18		y = 2  // Statement B
19		// A happens-before B
20	}()
21
22	// Rule 2: Send on channel happens-before receive completes
23	ch := make(chan int)
24	wg.Add(2)
25
26	go func() {
27		defer wg.Done()
28		x = 3
29		ch <- 1 // Send happens-before receive
30	}()
31
32	go func() {
33		defer wg.Done()
34		<-ch // Receive
35		fmt.Println(x) // Guaranteed to see x = 3
36	}()
37
38	// Rule 3: Unlock happens-before any subsequent lock
39	var mu sync.Mutex
40	wg.Add(2)
41
42	go func() {
43		defer wg.Done()
44		mu.Lock()
45		x = 4
46		mu.Unlock() // Unlock happens-before next Lock
47	}()
48
49	go func() {
50		defer wg.Done()
51		mu.Lock() // This Lock happens-after above Unlock
52		fmt.Println(x) // Guaranteed to see x = 4
53		mu.Unlock()
54	}()
55
56	wg.Wait()
57}
58
59func main() {
60	demonstrateHappensBefore()
61}

Real-world Example: In a web server, this ensures that when a request handler updates user session data, subsequent requests from the same user see those updates, preventing inconsistent session states.

Transitivity of Happens-Before

If A happens-before B and B happens-before C, then A happens-before C.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9// Transitivity demonstration
10func demonstrateTransitivity() {
11	var x int
12	ch1 := make(chan int)
13	ch2 := make(chan int)
14
15	// Goroutine A
16	go func() {
17		x = 42        // Operation 1
18		ch1 <- 1      // Operation 2: Signals B
19	}()
20
21	// Goroutine B
22	go func() {
23		<-ch1         // Waits for A (Operation 2 happens-before this)
24		// Now we can see x = 42 (Operation 1 happens-before Operation 2)
25		ch2 <- 1      // Operation 3: Signals C
26	}()
27
28	// Goroutine C
29	go func() {
30		<-ch2         // Waits for B (Operation 3 happens-before this)
31		// Now we can see x = 42 due to transitivity:
32		// Operation 1 happens-before Operation 2
33		// Operation 2 happens-before receive in B
34		// B's send Operation 3 happens-before receive in C
35		fmt.Println("x =", x) // Guaranteed to see 42
36	}()
37
38	// Wait for completion
39	var wg sync.WaitGroup
40	wg.Add(1)
41	go func() {
42		<-ch2
43		wg.Done()
44	}()
45	wg.Wait()
46}
47
48func main() {
49	demonstrateTransitivity()
50}

Synchronization Primitives

🎯 Practical Analogy: Synchronization primitives are like traffic signals for concurrent programming. Just as traffic lights prevent car accidents, these primitives prevent data accidents in concurrent code.

Mutex

💡 Key Takeaway: A mutex is like a bathroom key—only one person can use it at a time, and everyone knows when it's occupied or available.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9type SafeCounter struct {
10	mu    sync.Mutex
11	count int
12}
13
14func (c *SafeCounter) Increment() {
15	c.mu.Lock()
16	c.count++ // Protected by mutex
17	c.mu.Unlock()
18}
19
20func (c *SafeCounter) Value() int {
21	c.mu.Lock()
22	defer c.mu.Unlock()
23	return c.count
24}
25
26func main() {
27	counter := &SafeCounter{}
28	var wg sync.WaitGroup
29
30	// 100 goroutines incrementing
31	for i := 0; i < 100; i++ {
32		wg.Add(1)
33		go func() {
34			defer wg.Done()
35			for j := 0; j < 100; j++ {
36				counter.Increment()
37			}
38		}()
39	}
40
41	wg.Wait()
42	fmt.Println("Count:", counter.Value()) // Always 10000
43}

Real-world Example: In a banking system, mutexes protect account balances during transfers, preventing double-spending. When Alice transfers money to Bob, both accounts are locked until the transfer completes, ensuring no other operations can interfere.

RWMutex

🎯 Practical Analogy: An RWMutex is like a library reference section—many people can read simultaneously, but only one person can write.

💡 Key Takeaway: Use RWMutex when reads are much more frequent than writes. It allows multiple readers to proceed concurrently while still protecting against writer interference.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"time"
 8)
 9
10type Cache struct {
11	mu    sync.RWMutex
12	data  map[string]string
13}
14
15func NewCache() *Cache {
16	return &Cache{
17		data: make(map[string]string),
18	}
19}
20
21// Multiple readers can hold RLock simultaneously
22func (c *Cache) Get(key string) (string, bool) {
23	c.mu.RLock()
24	defer c.mu.RUnlock()
25	val, ok := c.data[key]
26	return val, ok
27}
28
29// Writer needs exclusive Lock
30func (c *Cache) Set(key, value string) {
31	c.mu.Lock()
32	defer c.mu.Unlock()
33	c.data[key] = value
34}
35
36func main() {
37	cache := NewCache()
38	var wg sync.WaitGroup
39
40	// Writer goroutine
41	wg.Add(1)
42	go func() {
43		defer wg.Done()
44		for i := 0; i < 10; i++ {
45			cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
46			time.Sleep(10 * time.Millisecond)
47		}
48	}()
49
50	// Multiple reader goroutines
51	for i := 0; i < 5; i++ {
52		wg.Add(1)
53		go func(id int) {
54			defer wg.Done()
55			for j := 0; j < 20; j++ {
56				if val, ok := cache.Get("key5"); ok {
57					fmt.Printf("Reader %d got: %s\n", id, val)
58				}
59				time.Sleep(5 * time.Millisecond)
60			}
61		}(i)
62	}
63
64	wg.Wait()
65}

Real-world Example: Content delivery networks use RWMutex-like patterns. Millions of users read cached content simultaneously, while content updates happen much less frequently. This allows massive read scalability while still protecting content integrity during updates.

Channels

🎯 Practical Analogy: Channels are like mail carriers with guaranteed delivery. When you send a letter, you know it will be delivered before the recipient acts on it. The "happens-before" guarantee ensures the message is received intact.

💡 Key Takeaway: Channels provide both communication and synchronization. The act of sending and receiving establishes a happens-before relationship, making them perfect for coordinating goroutines.

 1// run
 2package main
 3
 4import "fmt"
 5
 6// Channels provide happens-before guarantees
 7
 8func channelSynchronization() {
 9	// Unbuffered channel: send happens-before receive completes
10	ch1 := make(chan int)
11	var value int
12
13	go func() {
14		value = 42
15		ch1 <- 1 // Send
16	}()
17
18	<-ch1 // Receive happens-after send
19	fmt.Println(value) // Guaranteed to see 42
20
21	// Buffered channel: receive happens-before send completes
22	ch2 := make(chan int, 1)
23	ch2 <- 10
24
25	go func() {
26		<-ch2 // Receive
27		value = 100
28	}()
29
30	ch2 <- 20 // This send blocks until receive happens
31	fmt.Println(value) // Guaranteed to see 100
32}
33
34func main() {
35	channelSynchronization()
36}

Real-world Example: In Kubernetes, channels coordinate between controllers and the API server. When a controller detects a change, the API server processes it, ensuring state changes are applied in the correct order across the cluster.

sync.Once

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9var (
10	instance *Singleton
11	once     sync.Once
12)
13
14type Singleton struct {
15	data string
16}
17
18// Thread-safe singleton initialization
19func GetInstance() *Singleton {
20	once.Do(func() {
21		fmt.Println("Initializing singleton...")
22		instance = &Singleton{data: "I'm a singleton!"}
23	})
24	return instance
25}
26
27func main() {
28	var wg sync.WaitGroup
29
30	// Multiple goroutines try to get instance
31	for i := 0; i < 10; i++ {
32		wg.Add(1)
33		go func(id int) {
34			defer wg.Done()
35			inst := GetInstance()
36			fmt.Printf("Goroutine %d got: %s\n", id, inst.data)
37		}(i)
38	}
39
40	wg.Wait()
41	// "Initializing singleton..." printed only once
42}

sync.WaitGroup

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"time"
 8)
 9
10func worker(id int, wg *sync.WaitGroup) {
11	defer wg.Done() // Decrements counter when done
12
13	fmt.Printf("Worker %d starting\n", id)
14	time.Sleep(time.Second)
15	fmt.Printf("Worker %d done\n", id)
16}
17
18func main() {
19	var wg sync.WaitGroup
20
21	for i := 1; i <= 5; i++ {
22		wg.Add(1) // Increment counter
23		go worker(i, &wg)
24	}
25
26	wg.Wait() // Block until counter reaches 0
27	fmt.Println("All workers completed")
28}

sync.Cond - Condition Variables

Condition variables allow goroutines to wait for specific conditions to become true.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"time"
 8)
 9
10type Queue struct {
11	mu    sync.Mutex
12	cond  *sync.Cond
13	items []int
14}
15
16func NewQueue() *Queue {
17	q := &Queue{
18		items: make([]int, 0),
19	}
20	q.cond = sync.NewCond(&q.mu)
21	return q
22}
23
24func (q *Queue) Enqueue(item int) {
25	q.mu.Lock()
26	defer q.mu.Unlock()
27
28	q.items = append(q.items, item)
29	q.cond.Signal() // Wake up one waiting goroutine
30}
31
32func (q *Queue) Dequeue() int {
33	q.mu.Lock()
34	defer q.mu.Unlock()
35
36	// Wait while queue is empty
37	for len(q.items) == 0 {
38		q.cond.Wait() // Releases lock and waits
39	}
40
41	item := q.items[0]
42	q.items = q.items[1:]
43	return item
44}
45
46func main() {
47	queue := NewQueue()
48	var wg sync.WaitGroup
49
50	// Consumer
51	wg.Add(1)
52	go func() {
53		defer wg.Done()
54		for i := 0; i < 5; i++ {
55			item := queue.Dequeue()
56			fmt.Printf("Consumed: %d\n", item)
57		}
58	}()
59
60	// Producer
61	wg.Add(1)
62	go func() {
63		defer wg.Done()
64		for i := 1; i <= 5; i++ {
65			time.Sleep(500 * time.Millisecond)
66			queue.Enqueue(i)
67			fmt.Printf("Produced: %d\n", i)
68		}
69	}()
70
71	wg.Wait()
72}

Memory Ordering

Sequential Consistency

⚠️ Important: Go does NOT guarantee sequential consistency! This is a critical difference from languages like Java. Without proper synchronization, the compiler and CPU can reorder operations in surprising ways.

🎯 Practical Analogy: Think of memory ordering like a queue at a coffee shop. Without proper rules, people might cut in line, and the order of service becomes unpredictable. Synchronization establishes clear ordering rules.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"runtime"
 7	"sync"
 8	"sync/atomic"
 9)
10
11// WITHOUT atomic
12func withoutAtomic() {
13	var x, y, a, b int
14
15	go func() {
16		x = 1
17		a = y
18	}()
19
20	go func() {
21		y = 1
22		b = x
23	}()
24
25	runtime.Gosched()
26
27	// Possible outcomes:
28	// a=0, b=1
29	// a=1, b=0
30	// a=1, b=1
31	// a=0, b=0 ← This is possible due to reordering!
32
33	fmt.Printf("a=%d, b=%d\n", a, b)
34}
35
36// WITH atomic
37func withAtomic() {
38	var x, y atomic.Int32
39
40	var wg sync.WaitGroup
41	wg.Add(2)
42
43	go func() {
44		defer wg.Done()
45		x.Store(1)
46		a := y.Load()
47		fmt.Printf("a=%d\n", a)
48	}()
49
50	go func() {
51		defer wg.Done()
52		y.Store(1)
53		b := x.Load()
54		fmt.Printf("b=%d\n", b)
55	}()
56
57	wg.Wait()
58}
59
60func main() {
61	fmt.Println("Without atomic:")
62	withoutAtomic()
63
64	fmt.Println("\nWith atomic:")
65	withAtomic()
66}

Real-world Example: In a stock trading system, without proper memory ordering, different threads might see different orderings of trades, leading to inconsistent pricing and regulatory violations. Atomic operations ensure all traders see the same sequence of events.

Atomic Operations

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"sync/atomic"
 8)
 9
10type Metrics struct {
11	requests atomic.Int64
12	errors   atomic.Int64
13}
14
15func (m *Metrics) RecordRequest(success bool) {
16	m.requests.Add(1)
17	if !success {
18		m.errors.Add(1)
19	}
20}
21
22func (m *Metrics) ErrorRate() float64 {
23	reqs := m.requests.Load()
24	errs := m.errors.Load()
25
26	if reqs == 0 {
27		return 0
28	}
29
30	return float64(errs) / float64(reqs)
31}
32
33func main() {
34	metrics := &Metrics{}
35	var wg sync.WaitGroup
36
37	// Simulate requests
38	for i := 0; i < 100; i++ {
39		wg.Add(1)
40		go func(id int) {
41			defer wg.Done()
42			success := id%10 != 0 // 10% error rate
43			metrics.RecordRequest(success)
44		}(i)
45	}
46
47	wg.Wait()
48
49	fmt.Printf("Total requests: %d\n", metrics.requests.Load())
50	fmt.Printf("Total errors: %d\n", metrics.errors.Load())
51	fmt.Printf("Error rate: %.2f%%\n", metrics.ErrorRate()*100)
52}

Memory Order Semantics

Different atomic operations provide different ordering guarantees:

Sequential Consistency (strongest):

  • All operations appear in some total order
  • All processors agree on this order
  • Example: atomic.LoadInt32, atomic.StoreInt32

Acquire-Release (medium):

  • Acquire loads see all prior releases
  • Release stores are visible to subsequent acquires
  • Example: sync.Mutex.Lock() (acquire), sync.Mutex.Unlock() (release)

Relaxed (weakest):

  • No ordering guarantees beyond atomicity
  • Only guarantees individual operation is atomic
  • Example: Some internal runtime operations
 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"sync/atomic"
 8)
 9
10type SharedData struct {
11	ready atomic.Bool
12	value int
13}
14
15// Publish-subscribe pattern with proper memory ordering
16func demonstrateAcquireRelease() {
17	data := &SharedData{}
18	var wg sync.WaitGroup
19
20	// Publisher (uses release semantics)
21	wg.Add(1)
22	go func() {
23		defer wg.Done()
24
25		// Prepare data
26		data.value = 42
27
28		// Release: All prior writes are visible after this
29		data.ready.Store(true)
30		fmt.Println("Publisher: Data published")
31	}()
32
33	// Subscriber (uses acquire semantics)
34	wg.Add(1)
35	go func() {
36		defer wg.Done()
37
38		// Acquire: See all writes that happened before release
39		for !data.ready.Load() {
40			// Spin wait
41		}
42
43		fmt.Printf("Subscriber: Read value = %d\n", data.value)
44	}()
45
46	wg.Wait()
47}
48
49func main() {
50	demonstrateAcquireRelease()
51}

Race Condition Prevention

🎯 Practical Analogy: Race conditions are like multiple chefs adding ingredients to the same recipe without coordination—one chef might add salt twice while another skips it entirely, ruining the dish.

Common Race Patterns

⚠️ Important: These are the most common sources of bugs in concurrent Go programs. The race detector is your best friend for catching these early.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9// WRONG: Map race
10func mapRaceWrong() {
11	m := make(map[int]int)
12
13	for i := 0; i < 100; i++ {
14		go func(n int) {
15			m[n] = n // DATA RACE!
16		}(i)
17	}
18}
19
20// CORRECT: Map with mutex
21func mapRaceFixed() {
22	m := make(map[int]int)
23	var mu sync.Mutex
24
25	var wg sync.WaitGroup
26	for i := 0; i < 100; i++ {
27		wg.Add(1)
28		go func(n int) {
29			defer wg.Done()
30			mu.Lock()
31			m[n] = n // Protected
32			mu.Unlock()
33		}(i)
34	}
35	wg.Wait()
36
37	fmt.Println("Map size:", len(m))
38}
39
40// WRONG: Slice race
41func sliceRaceWrong() {
42	var slice []int
43
44	for i := 0; i < 100; i++ {
45		go func(n int) {
46			slice = append(slice, n) // DATA RACE!
47		}(i)
48	}
49}
50
51// CORRECT: Slice with mutex
52func sliceRaceFixed() {
53	var slice []int
54	var mu sync.Mutex
55
56	var wg sync.WaitGroup
57	for i := 0; i < 100; i++ {
58		wg.Add(1)
59		go func(n int) {
60			defer wg.Done()
61			mu.Lock()
62			slice = append(slice, n) // Protected
63			mu.Unlock()
64		}(i)
65	}
66	wg.Wait()
67
68	fmt.Println("Slice length:", len(slice))
69}
70
71func main() {
72	fmt.Println("=== Race Condition Examples ===")
73
74	// mapRaceWrong() // Would cause race
75	mapRaceFixed()
76
77	// sliceRaceWrong() // Would cause race
78	sliceRaceFixed()
79}

💡 Key Takeaway: Go maps are not concurrent-safe! Always protect them with mutexes. The same goes for slices and any data structure that can be modified concurrently.

Closure Variable Capture

🎯 Practical Analogy: Capturing loop variables is like giving multiple people the same shopping list that keeps changing. Everyone ends up buying the same final item, not what was on the list when they started shopping.

💡 Key Takeaway: This is one of the most common Go beginner mistakes. Always pass loop variables as parameters to goroutines or create local copies.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9// WRONG: Capturing loop variable
10func closureCaptureWrong() {
11	var wg sync.WaitGroup
12
13	for i := 0; i < 5; i++ {
14		wg.Add(1)
15		go func() {
16			defer wg.Done()
17			fmt.Println(i) // Captures loop variable
18		}()
19	}
20
21	wg.Wait()
22	// Prints unpredictable values, often all 5s
23}
24
25// CORRECT: Pass as parameter
26func closureCaptureFixed1() {
27	var wg sync.WaitGroup
28
29	for i := 0; i < 5; i++ {
30		wg.Add(1)
31		go func(n int) {
32			defer wg.Done()
33			fmt.Println(n) // Each goroutine gets its own copy
34		}(i)
35	}
36
37	wg.Wait()
38	// Prints 0, 1, 2, 3, 4 in some order
39}
40
41// CORRECT: Create local copy
42func closureCaptureFixed2() {
43	var wg sync.WaitGroup
44
45	for i := 0; i < 5; i++ {
46		i := i // Create local copy
47		wg.Add(1)
48		go func() {
49			defer wg.Done()
50			fmt.Println(i)
51		}()
52	}
53
54	wg.Wait()
55}
56
57func main() {
58	fmt.Println("Wrong:")
59	closureCaptureWrong()
60
61	fmt.Println("\nFixed:")
62	closureCaptureFixed1()
63
64	fmt.Println("\nFixed:")
65	closureCaptureFixed2()
66}

Real-world Example: In a web crawler, this bug caused all goroutines to process the same URL instead of distributing the work, severely limiting crawling efficiency.

Double-Checked Locking

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9type Resource struct {
10	data string
11}
12
13// WRONG: Double-checked locking without proper synchronization
14func wrongDoubleCheck() *Resource {
15	var resource *Resource
16	var mu sync.Mutex
17
18	initialize := func() *Resource {
19		mu.Lock()
20		defer mu.Unlock()
21
22		if resource == nil { // Check again under lock
23			resource = &Resource{data: "initialized"}
24		}
25
26		return resource
27	}
28
29	// First check without lock
30	if resource == nil {
31		return initialize()
32	}
33
34	return resource
35}
36
37// CORRECT: Use sync.Once
38func correctDoubleCheck() *Resource {
39	var resource *Resource
40	var once sync.Once
41
42	once.Do(func() {
43		resource = &Resource{data: "initialized"}
44	})
45
46	return resource
47}
48
49func main() {
50	var wg sync.WaitGroup
51
52	// Test with sync.Once
53	for i := 0; i < 10; i++ {
54		wg.Add(1)
55		go func() {
56			defer wg.Done()
57			res := correctDoubleCheck()
58			fmt.Printf("Got resource: %s\n", res.data)
59		}()
60	}
61
62	wg.Wait()
63}

Data Race Detection

Go provides a built-in race detector to find data races at runtime.

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9func demonstrateRaceDetector() {
10	// To detect races: go run -race main.go
11
12	var counter int
13	var wg sync.WaitGroup
14
15	// This will be detected by race detector
16	for i := 0; i < 10; i++ {
17		wg.Add(1)
18		go func() {
19			defer wg.Done()
20			counter++ // DATA RACE!
21		}()
22	}
23
24	wg.Wait()
25	fmt.Println("Counter:", counter)
26}
27
28func main() {
29	fmt.Println("=== Race Detector Demo ===")
30	fmt.Println("Run with: go run -race main.go")
31	demonstrateRaceDetector()
32}

How to Use the Race Detector:

1# Run with race detection
2go run -race main.go
3
4# Test with race detection
5go test -race ./...
6
7# Build with race detection
8go build -race

Race Detector Output Example:

==================
WARNING: DATA RACE
Write at 0x00c000018098 by goroutine 7:
  main.main.func1()
      /path/to/main.go:15 +0x3c

Previous write at 0x00c000018098 by goroutine 6:
  main.main.func1()
      /path/to/main.go:15 +0x3c
==================

Production Patterns

Message Passing

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"time"
 7)
 8
 9type Task struct {
10	ID   int
11	Data string
12}
13
14type Result struct {
15	TaskID int
16	Output string
17}
18
19// Worker pool using channels
20func messagePassingPattern() {
21	tasks := make(chan Task, 10)
22	results := make(chan Result, 10)
23
24	// Start workers
25	for w := 1; w <= 3; w++ {
26		go worker(w, tasks, results)
27	}
28
29	// Send tasks
30	go func() {
31		for i := 1; i <= 5; i++ {
32			tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
33		}
34		close(tasks)
35	}()
36
37	// Collect results
38	for i := 1; i <= 5; i++ {
39		result := <-results
40		fmt.Printf("Result %d: %s\n", result.TaskID, result.Output)
41	}
42}
43
44func worker(id int, tasks <-chan Task, results chan<- Result) {
45	for task := range tasks {
46		fmt.Printf("Worker %d processing task %d\n", id, task.ID)
47		time.Sleep(100 * time.Millisecond)
48
49		results <- Result{
50			TaskID: task.ID,
51			Output: fmt.Sprintf("processed by worker %d", id),
52		}
53	}
54}
55
56func main() {
57	messagePassingPattern()
58}

Read-Copy-Update

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"sync/atomic"
 8	"unsafe"
 9)
10
11// RCU pattern for high-read, low-write scenarios
12type ConfigRCU struct {
13	current unsafe.Pointer // *Config
14}
15
16type Config struct {
17	Value string
18}
19
20func NewConfigRCU(initial *Config) *ConfigRCU {
21	return &ConfigRCU{
22		current: unsafe.Pointer(initial),
23	}
24}
25
26// Read
27func (c *ConfigRCU) Get() *Config {
28	return (*Config)(atomic.LoadPointer(&c.current))
29}
30
31// Write
32func (c *ConfigRCU) Update(newConfig *Config) {
33	atomic.StorePointer(&c.current, unsafe.Pointer(newConfig))
34}
35
36func main() {
37	config := NewConfigRCU(&Config{Value: "v1"})
38
39	var wg sync.WaitGroup
40
41	// Many readers
42	for i := 0; i < 10; i++ {
43		wg.Add(1)
44		go func(id int) {
45			defer wg.Done()
46			for j := 0; j < 100; j++ {
47				cfg := config.Get()
48				fmt.Printf("Reader %d: %s\n", id, cfg.Value)
49			}
50		}(i)
51	}
52
53	// One writer
54	wg.Add(1)
55	go func() {
56		defer wg.Done()
57		for i := 2; i <= 5; i++ {
58			config.Update(&Config{Value: fmt.Sprintf("v%d", i)})
59		}
60	}()
61
62	wg.Wait()
63}

Lock-Free Stack

 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"sync/atomic"
 8	"unsafe"
 9)
10
11// Node in the stack
12type Node struct {
13	value int
14	next  *Node
15}
16
17// LockFreeStack implements a lock-free stack using CAS
18type LockFreeStack struct {
19	head unsafe.Pointer // *Node
20}
21
22func NewLockFreeStack() *LockFreeStack {
23	return &LockFreeStack{}
24}
25
26// Push adds a value to the stack
27func (s *LockFreeStack) Push(value int) {
28	node := &Node{value: value}
29
30	for {
31		old := atomic.LoadPointer(&s.head)
32		node.next = (*Node)(old)
33
34		if atomic.CompareAndSwapPointer(&s.head, old, unsafe.Pointer(node)) {
35			return
36		}
37	}
38}
39
40// Pop removes and returns a value from the stack
41func (s *LockFreeStack) Pop() (int, bool) {
42	for {
43		old := atomic.LoadPointer(&s.head)
44		if old == nil {
45			return 0, false
46		}
47
48		node := (*Node)(old)
49		next := unsafe.Pointer(node.next)
50
51		if atomic.CompareAndSwapPointer(&s.head, old, next) {
52			return node.value, true
53		}
54	}
55}
56
57func main() {
58	stack := NewLockFreeStack()
59	var wg sync.WaitGroup
60
61	// Concurrent pushes
62	for i := 0; i < 10; i++ {
63		wg.Add(1)
64		go func(val int) {
65			defer wg.Done()
66			stack.Push(val)
67			fmt.Printf("Pushed: %d\n", val)
68		}(i)
69	}
70
71	wg.Wait()
72
73	// Concurrent pops
74	for i := 0; i < 10; i++ {
75		wg.Add(1)
76		go func() {
77			defer wg.Done()
78			if val, ok := stack.Pop(); ok {
79				fmt.Printf("Popped: %d\n", val)
80			}
81		}()
82	}
83
84	wg.Wait()
85}

Practice Exercises

Exercise 1: Fix the Race

🎯 Learning Objectives:

  • Identify common data race patterns in concurrent Go code
  • Master mutex and atomic operation solutions
  • Understand when to use different synchronization primitives
  • Practice race detection and debugging techniques

🌍 Real-World Context:
Data races are among the most dangerous and costly bugs in concurrent systems. A famous incident at Cloudflare in 2017 caused a global service outage when a race condition in their routing software sent traffic to wrong servers. Similarly, Uber experienced payment processing races that double-charged customers. Learning to spot and fix data races is essential for building reliable concurrent systems.

⏱️ Time Estimate: 30-45 minutes
📊 Difficulty: Intermediate

Find and fix the data race in the provided code. This classic counter race demonstrates how concurrent increments without proper synchronization can lead to incorrect results. Your solution should ensure thread safety while maintaining good performance.

 1// run
 2package main
 3
 4import "fmt"
 5
 6var counter int
 7
 8func main() {
 9	for i := 0; i < 1000; i++ {
10		go func() {
11			counter++ // Race!
12		}()
13	}
14
15	fmt.Println(counter)
16}
Solution
 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7	"sync/atomic"
 8)
 9
10// Solution 1: Using mutex
11func withMutex() {
12	var counter int
13	var mu sync.Mutex
14	var wg sync.WaitGroup
15
16	for i := 0; i < 1000; i++ {
17		wg.Add(1)
18		go func() {
19			defer wg.Done()
20			mu.Lock()
21			counter++
22			mu.Unlock()
23		}()
24	}
25
26	wg.Wait()
27	fmt.Println("With mutex:", counter)
28}
29
30// Solution 2: Using atomic
31func withAtomic() {
32	var counter atomic.Int64
33	var wg sync.WaitGroup
34
35	for i := 0; i < 1000; i++ {
36		wg.Add(1)
37		go func() {
38			defer wg.Done()
39			counter.Add(1)
40		}()
41	}
42
43	wg.Wait()
44	fmt.Println("With atomic:", counter.Load())
45}
46
47func main() {
48	withMutex()
49	withAtomic()
50}

Exercise 2: Implement a Thread-Safe Cache

🎯 Learning Objectives:

  • Implement read-write mutex patterns for optimal performance
  • Design thread-safe data structures with proper concurrency
  • Understand RWMutex vs Mutex trade-offs
  • Practice concurrent access patterns for read-heavy workloads

🌍 Real-World Context:
Thread-safe caches are fundamental building blocks in high-performance systems. Companies like Redis and MemSQL build entire products around concurrent caching patterns. In web services, caching reduces database load by 60-80% and improves response times dramatically. A well-designed concurrent cache can handle millions of requests per second while maintaining data consistency.

⏱️ Time Estimate: 45-60 minutes
📊 Difficulty: Intermediate

Create a thread-safe cache with Get and Set methods that optimizes for read-heavy workloads using RWMutex. Your implementation should handle concurrent reads without blocking while ensuring writes are safely synchronized, demonstrating the power of read-write locks for caching scenarios.

Solution
 1// run
 2package main
 3
 4import (
 5	"fmt"
 6	"sync"
 7)
 8
 9type Cache struct {
10	mu   sync.RWMutex
11	data map[string]interface{}
12}
13
14func NewCache() *Cache {
15	return &Cache{
16		data: make(map[string]interface{}),
17	}
18}
19
20func (c *Cache) Get(key string) (interface{}, bool) {
21	c.mu.RLock()
22	defer c.mu.RUnlock()
23	val, ok := c.data[key]
24	return val, ok
25}
26
27func (c *Cache) Set(key string, value interface{}) {
28	c.mu.Lock()
29	defer c.mu.Unlock()
30	c.data[key] = value
31}
32
33func main() {
34	cache := NewCache()
35	var wg sync.WaitGroup
36
37	// Writers
38	for i := 0; i < 10; i++ {
39		wg.Add(1)
40		go func(n int) {
41			defer wg.Done()
42			cache.Set(fmt.Sprintf("key%d", n), n)
43		}(i)
44	}
45
46	// Readers
47	for i := 0; i < 10; i++ {
48		wg.Add(1)
49		go func(n int) {
50			defer wg.Done()
51			if val, ok := cache.Get(fmt.Sprintf("key%d", n)); ok {
52				fmt.Printf("Got: %v\n", val)
53			}
54		}(i)
55	}
56
57	wg.Wait()
58}

Exercise 3: Happens-Before Detector

🎯 Learning Objectives:

  • Deeply understand happens-before relationships in Go's memory model
  • Visualize and demonstrate causal relationships between concurrent operations
  • Master different synchronization primitives and their guarantees
  • Learn to reason about memory ordering in concurrent systems

🌍 Real-World Context:
Understanding happens-before relationships is crucial for debugging complex concurrent systems. In distributed systems like Kubernetes, understanding causal relationships between events is essential for maintaining cluster state consistency. Companies building distributed databases spend significant engineering effort on ensuring proper happens-before guarantees to prevent data corruption and maintain consistency across nodes.

⏱️ Time Estimate: 60-90 minutes
📊 Difficulty: Advanced

Build a tool that demonstrates happens-before relationships between goroutines using different synchronization primitives. Your detector should visualize event ordering, prove that synchronization primitives establish proper happens-before guarantees, and help developers understand when memory writes become visible across goroutines.

Solution with Explanation
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"sync"
  7	"sync/atomic"
  8	"time"
  9)
 10
 11type Event struct {
 12	id        int
 13	timestamp time.Time
 14	goroutine int
 15	message   string
 16}
 17
 18type HappensBeforeDetector struct {
 19	events []Event
 20	mu     sync.Mutex
 21	nextID atomic.Int32
 22}
 23
 24func NewDetector() *HappensBeforeDetector {
 25	return &HappensBeforeDetector{
 26		events: make([]Event, 0, 100),
 27	}
 28}
 29
 30func (d *HappensBeforeDetector) Record(goroutineID int, message string) {
 31	event := Event{
 32		id:        int(d.nextID.Add(1)),
 33		timestamp: time.Now(),
 34		goroutine: goroutineID,
 35		message:   message,
 36	}
 37
 38	d.mu.Lock()
 39	d.events = append(d.events, event)
 40	d.mu.Unlock()
 41}
 42
 43func (d *HappensBeforeDetector) Report() {
 44	d.mu.Lock()
 45	defer d.mu.Unlock()
 46
 47	fmt.Println("\n=== Happens-Before Event Timeline ===")
 48	for _, e := range d.events {
 49		fmt.Printf("[G%d] Event %d: %s @ %s\n",
 50			e.goroutine, e.id, e.message, e.timestamp.Format("15:04:05.000"))
 51	}
 52}
 53
 54// Scenario 1: Channel happens-before
 55func demonstrateChannelHB(d *HappensBeforeDetector) {
 56	fmt.Println("\n--- Scenario 1: Channel Happens-Before ---")
 57
 58	ch := make(chan int)
 59	var sharedData int
 60
 61	go func() {
 62		d.Record(1, "G1: Writing sharedData = 42")
 63		sharedData = 42
 64		d.Record(1, "G1: Sending on channel")
 65		ch <- 1 // Send happens-before receive completes
 66		d.Record(1, "G1: Send completed")
 67	}()
 68
 69	go func() {
 70		d.Record(2, "G2: Waiting on channel")
 71		<-ch // Receive
 72		d.Record(2, "G2: Received from channel")
 73		// Guaranteed to see sharedData = 42
 74		d.Record(2, fmt.Sprintf("G2: Reading sharedData = %d", sharedData))
 75	}()
 76
 77	time.Sleep(100 * time.Millisecond)
 78}
 79
 80// Scenario 2: Mutex happens-before
 81func demonstrateMutexHB(d *HappensBeforeDetector) {
 82	fmt.Println("\n--- Scenario 2: Mutex Happens-Before ---")
 83
 84	var mu sync.Mutex
 85	var counter int
 86
 87	go func() {
 88		d.Record(3, "G3: Acquiring lock")
 89		mu.Lock()
 90		d.Record(3, "G3: Lock acquired")
 91		counter = 100
 92		d.Record(3, "G3: Set counter = 100")
 93		mu.Unlock()
 94		d.Record(3, "G3: Lock released")
 95	}()
 96
 97	time.Sleep(50 * time.Millisecond)
 98
 99	go func() {
100		d.Record(4, "G4: Acquiring lock")
101		mu.Lock()
102		d.Record(4, "G4: Lock acquired")
103		d.Record(4, fmt.Sprintf("G4: Read counter = %d", counter))
104		mu.Unlock()
105		d.Record(4, "G4: Lock released")
106	}()
107
108	time.Sleep(100 * time.Millisecond)
109}
110
111// Scenario 3: WaitGroup happens-before
112func demonstrateWaitGroupHB(d *HappensBeforeDetector) {
113	fmt.Println("\n--- Scenario 3: WaitGroup Happens-Before ---")
114
115	var wg sync.WaitGroup
116	var results []int
117
118	for i := 1; i <= 3; i++ {
119		wg.Add(1)
120		go func(id int) {
121			defer wg.Done()
122			d.Record(id+4, fmt.Sprintf("G%d: Computing result", id+4))
123			time.Sleep(10 * time.Millisecond)
124			results = append(results, id*10)
125			d.Record(id+4, fmt.Sprintf("G%d: Done() called", id+4))
126		}(i)
127	}
128
129	d.Record(8, "G8: Calling Wait()")
130	wg.Wait()
131	d.Record(8, "G8: Wait() returned")
132	d.Record(8, fmt.Sprintf("G8: All results collected: %v", results))
133}
134
135func main() {
136	detector := NewDetector()
137
138	detector.Record(0, "Main: Starting happens-before detection")
139
140	demonstrateChannelHB(detector)
141	demonstrateMutexHB(detector)
142	demonstrateWaitGroupHB(detector)
143
144	time.Sleep(200 * time.Millisecond)
145	detector.Report()
146
147	fmt.Println("\n=== Happens-Before Guarantees ===")
148	fmt.Println("1. Channel send happens-before receive completes")
149	fmt.Println("2. Mutex unlock happens-before subsequent lock")
150	fmt.Println("3. WaitGroup Done() happens-before Wait() returns")
151	fmt.Println("4. Goroutine creation happens-before goroutine execution")
152}

Explanation:

This detector demonstrates happens-before relationships:

  1. Channel Synchronization: Write to sharedData happens-before send on channel, which happens-before receive completes. Therefore, receive is guaranteed to see the write.

  2. Mutex Synchronization: Write to counter inside critical section, unlock happens-before the next Lock. Second goroutine guaranteed to see the write.

  3. WaitGroup Synchronization: All Done() calls happen-before Wait() returns. Main goroutine guaranteed to see all writes from workers.

  4. Event Recording: Timestamp-based timeline shows ordering and demonstrates causal relationships between events.

Key Insight: Happens-before guarantees enable reasoning about concurrent code correctness without worrying about low-level memory ordering details.

Exercise 4: Race Condition Finder

🎯 Learning Objectives:

  • Master Go's race detector for identifying data races
  • Learn to recognize common race condition patterns
  • Practice systematic debugging of concurrent code
  • Understand the difference between symptom and root cause in concurrent bugs

🌍 Real-World Context:
Race conditions are notoriously difficult to debug because they're often intermittent and timing-dependent. Companies like Google have entire teams dedicated to race detection tools. The Go race detector has prevented countless production incidents by catching races early in development. Understanding how to use it effectively is a critical skill for any Go developer working on concurrent systems.

⏱️ Time Estimate: 75-90 minutes
📊 Difficulty: Advanced

Create a program that intentionally contains data races, then fix them using proper synchronization. Your race finder should demonstrate common race patterns, show how the Go race detector identifies them, and provide systematic fixes. This exercise teaches you to think like a race detector and prevent subtle bugs before they reach production.

Solution with Explanation
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"sync"
  7	"sync/atomic"
  8	"time"
  9)
 10
 11// Scenario 1: Map race
 12type BrokenCache struct {
 13	data map[string]int
 14}
 15
 16func (c *BrokenCache) IncreaseCount(key string) {
 17	c.data[key]++
 18}
 19
 20// Scenario 1: Map race FIXED
 21type SafeCache struct {
 22	mu   sync.RWMutex
 23	data map[string]int
 24}
 25
 26func NewSafeCache() *SafeCache {
 27	return &SafeCache{
 28		data: make(map[string]int),
 29	}
 30}
 31
 32func (c *SafeCache) IncreaseCount(key string) {
 33	c.mu.Lock()
 34	c.data[key]++
 35	c.mu.Unlock()
 36}
 37
 38func (c *SafeCache) Get(key string) int {
 39	c.mu.RLock()
 40	defer c.mu.RUnlock()
 41	return c.data[key]
 42}
 43
 44// Scenario 2: Counter race
 45type BrokenCounter struct {
 46	value int
 47}
 48
 49func (c *BrokenCounter) Increment() {
 50	c.value++
 51}
 52
 53// Scenario 2: Counter race FIXED
 54type SafeCounter struct {
 55	value atomic.Int64
 56}
 57
 58func (c *SafeCounter) Increment() {
 59	c.value.Add(1)
 60}
 61
 62func (c *SafeCounter) Value() int64 {
 63	return c.value.Load()
 64}
 65
 66// Scenario 3: Closure capture FIXED
 67func safeClosureCapture() []int {
 68	var mu sync.Mutex
 69	var results []int
 70	var wg sync.WaitGroup
 71
 72	for i := 0; i < 5; i++ {
 73		wg.Add(1)
 74		go func(n int) {
 75			defer wg.Done()
 76			mu.Lock()
 77			results = append(results, n)
 78			mu.Unlock()
 79		}(i)
 80	}
 81
 82	wg.Wait()
 83	return results
 84}
 85
 86// Race detector helper
 87func detectRaces(name string, fn func()) {
 88	fmt.Printf("\n=== %s ===\n", name)
 89	fmt.Println("Running test...")
 90
 91	done := make(chan bool)
 92	go func() {
 93		fn()
 94		done <- true
 95	}()
 96
 97	select {
 98	case <-done:
 99		fmt.Println("Test completed")
100	case <-time.After(time.Second):
101		fmt.Println("Test timeout")
102	}
103}
104
105func main() {
106	fmt.Println("=== Race Condition Finder ===")
107	fmt.Println("Run with: go run -race main.go")
108	fmt.Println("(The race detector will report data races in BROKEN versions)\n")
109
110	// Test safe cache
111	detectRaces("FIXED: Safe Map", func() {
112		cache := NewSafeCache()
113		var wg sync.WaitGroup
114
115		for i := 0; i < 10; i++ {
116			wg.Add(1)
117			go func() {
118				defer wg.Done()
119				cache.IncreaseCount("key")
120				_ = cache.Get("key")
121			}()
122		}
123		wg.Wait()
124		fmt.Printf("Final count: %d\n", cache.Get("key"))
125	})
126
127	// Test safe counter
128	detectRaces("FIXED: Safe Counter", func() {
129		counter := &SafeCounter{}
130		var wg sync.WaitGroup
131
132		for i := 0; i < 100; i++ {
133			wg.Add(1)
134			go func() {
135				defer wg.Done()
136				counter.Increment()
137			}()
138		}
139		wg.Wait()
140		fmt.Printf("Final value: %d\n", counter.Value())
141	})
142
143	// Test closure capture
144	fmt.Println("\n=== FIXED: Safe Closure ===")
145	results := safeClosureCapture()
146	fmt.Printf("Results: %v\n", results)
147
148	fmt.Println("\n=== Summary ===")
149	fmt.Println("Common Race Conditions:")
150	fmt.Println("1. Concurrent map access → Use sync.RWMutex")
151	fmt.Println("2. Counter increment → Use atomic operations")
152	fmt.Println("3. Closure variable capture → Pass as parameter")
153	fmt.Println("4. Slice append → Use mutex protection")
154	fmt.Println("\nDetection: Run tests with `go test -race`")
155}

Explanation:

This exercise demonstrates common race conditions and their fixes:

  1. Map Race: Concurrent reads/writes to map cause crashes. Fix with sync.RWMutex.

  2. Counter Race: counter++ is read-modify-write. Fix with atomic.Int64.

  3. Closure Capture: All goroutines capture same loop variable. Fix by passing value as parameter.

  4. Race Detector: Use go run -race to detect data races at runtime.

Production Impact: Data races cause subtle bugs. Always test with -race flag during development.

Exercise 5: Channel Synchronization

🎯 Learning Objectives:

  • Master producer-consumer patterns using Go channels
  • Understand the synchronization guarantees of different channel types
  • Learn to design concurrent pipelines with proper backpressure
  • Practice channel-based coordination and shutdown patterns

🌍 Real-World Context:
Producer-consumer pipelines are the backbone of many production systems. Companies like Netflix use channel-based pipelines for video processing, while data streaming platforms like Kafka implement similar patterns at massive scale. Understanding channel synchronization is essential for building high-throughput systems that can gracefully handle backpressure and shutdown cleanly without data loss.

⏱️ Time Estimate: 90-120 minutes
📊 Difficulty: Expert

Implement a producer-consumer pipeline using channels with proper synchronization, demonstrating the happens-before guarantees of different channel types. Your pipeline should handle concurrent producers and consumers, implement graceful shutdown, show the differences between buffered and unbuffered channels, and demonstrate proper coordination patterns used in production systems.

Solution with Explanation
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"sync"
  7	"time"
  8)
  9
 10type Task struct {
 11	ID   int
 12	Data string
 13}
 14
 15type Result struct {
 16	TaskID int
 17	Output string
 18	Worker int
 19}
 20
 21type Pipeline struct {
 22	tasks   chan Task
 23	results chan Result
 24	done    chan struct{}
 25}
 26
 27func NewPipeline(bufferSize int) *Pipeline {
 28	return &Pipeline{
 29		tasks:   make(chan Task, bufferSize),
 30		results: make(chan Result, bufferSize),
 31		done:    make(chan struct{}),
 32	}
 33}
 34
 35func (p *Pipeline) Produce(count int) {
 36	fmt.Printf("Producer: Starting to produce %d tasks\n", count)
 37
 38	for i := 1; i <= count; i++ {
 39		task := Task{
 40			ID:   i,
 41			Data: fmt.Sprintf("task-%d", i),
 42		}
 43
 44		fmt.Printf("Producer: Sending task %d\n", task.ID)
 45		p.tasks <- task
 46
 47		time.Sleep(50 * time.Millisecond)
 48	}
 49
 50	close(p.tasks)
 51	fmt.Println("Producer: All tasks sent, channel closed")
 52}
 53
 54func (p *Pipeline) Consume(workerID int, wg *sync.WaitGroup) {
 55	defer wg.Done()
 56
 57	fmt.Printf("Worker %d: Started\n", workerID)
 58
 59	for task := range p.tasks {
 60		fmt.Printf("Worker %d: Processing task %d\n", workerID, task.ID)
 61
 62		time.Sleep(100 * time.Millisecond)
 63
 64		result := Result{
 65			TaskID: task.ID,
 66			Output: fmt.Sprintf("processed-%s", task.Data),
 67			Worker: workerID,
 68		}
 69
 70		p.results <- result
 71		fmt.Printf("Worker %d: Completed task %d\n", workerID, task.ID)
 72	}
 73
 74	fmt.Printf("Worker %d: No more tasks, exiting\n", workerID)
 75}
 76
 77func (p *Pipeline) Collect(expected int) []Result {
 78	fmt.Println("Collector: Started")
 79
 80	var results []Result
 81
 82	for i := 0; i < expected; i++ {
 83		result := <-p.results
 84		results = append(results, result)
 85		fmt.Printf("Collector: Received result from task %d (worker %d)\n",
 86			result.TaskID, result.Worker)
 87	}
 88
 89	close(p.done)
 90	fmt.Println("Collector: All results collected")
 91
 92	return results
 93}
 94
 95func demonstrateChannelTypes() {
 96	fmt.Println("\n=== Unbuffered vs Buffered Channels ===\n")
 97
 98	fmt.Println("--- Unbuffered Channel ---")
 99	unbuffered := make(chan int)
100
101	go func() {
102		fmt.Println("Sender: Sending on unbuffered channel...")
103		unbuffered <- 42
104		fmt.Println("Sender: Send completed")
105	}()
106
107	time.Sleep(100 * time.Millisecond)
108	fmt.Println("Receiver: Receiving from unbuffered channel...")
109	val := <-unbuffered
110	fmt.Printf("Receiver: Got %d\n", val)
111
112	time.Sleep(100 * time.Millisecond)
113
114	fmt.Println("\n--- Buffered Channel ---")
115	buffered := make(chan int, 3)
116
117	go func() {
118		for i := 1; i <= 3; i++ {
119			fmt.Printf("Sender: Sending %d\n", i)
120			buffered <- i
121			fmt.Printf("Sender: Sent %d\n", i)
122		}
123
124		fmt.Println("Sender: Sending 4th item...")
125		buffered <- 4
126		fmt.Println("Sender: Sent 4th item")
127	}()
128
129	time.Sleep(100 * time.Millisecond)
130
131	for i := 1; i <= 4; i++ {
132		val := <-buffered
133		fmt.Printf("Receiver: Got %d\n", val)
134		time.Sleep(50 * time.Millisecond)
135	}
136}
137
138func main() {
139	fmt.Println("=== Channel Synchronization Pipeline ===\n")
140
141	pipeline := NewPipeline(5)
142
143	const numTasks = 10
144	const numWorkers = 3
145
146	var wg sync.WaitGroup
147	for i := 1; i <= numWorkers; i++ {
148		wg.Add(1)
149		go pipeline.Consume(i, &wg)
150	}
151
152	resultsChan := make(chan []Result)
153	go func() {
154		results := pipeline.Collect(numTasks)
155		resultsChan <- results
156	}()
157
158	go pipeline.Produce(numTasks)
159
160	wg.Wait()
161	fmt.Println("\nAll workers finished")
162
163	results := <-resultsChan
164	<-pipeline.done
165
166	fmt.Printf("\n=== Pipeline Complete ===\n")
167	fmt.Printf("Total tasks processed: %d\n", len(results))
168
169	workerCounts := make(map[int]int)
170	for _, r := range results {
171		workerCounts[r.Worker]++
172	}
173
174	fmt.Println("\nWorker distribution:")
175	for worker, count := range workerCounts {
176		fmt.Printf("  Worker %d: %d tasks\n", worker, count)
177	}
178
179	demonstrateChannelTypes()
180
181	fmt.Println("\n=== Happens-Before Guarantees ===")
182	fmt.Println("1. Producer send happens-before consumer receive")
183	fmt.Println("2. Consumer send happens-before collector receive")
184	fmt.Println("3. Channel close happens-before range loop exits")
185	fmt.Println("4. All worker Done() happens-before wg.Wait() returns")
186}

Explanation:

This pipeline demonstrates channel synchronization:

  1. Producer-Consumer Pattern: Producer sends tasks, workers process them, collector gathers results with clean shutdown via channel close.

  2. Happens-Before with Channels: Send happens-before receive completes, guaranteeing visibility of all writes. No data races despite concurrent access.

  3. Unbuffered Channels: Send blocks until receive occurs, providing synchronization point where sender knows receiver has received.

  4. Buffered Channels: Send doesn't block until buffer full, still provides happens-before guarantees, better throughput for producer/consumer.

  5. Coordination: WaitGroup for worker completion, channel close for signaling, multiple synchronization primitives working together.

Production Use: This pattern is used for parallel processing with guaranteed correctness.

Exercise 6: Memory Barrier Visualizer

🎯 Learning Objectives:

  • Understand memory barriers and their effects on ordering
  • Visualize reordering allowed by weak memory models
  • Master the use of atomic operations for proper synchronization
  • Learn to reason about cross-goroutine visibility

🌍 Real-World Context:
Memory barriers are fundamental to understanding how modern processors optimize memory access. In high-performance systems like databases and trading platforms, understanding when and why memory barriers are needed prevents subtle bugs. For example, a missing memory barrier in a database transaction system could allow transactions to appear out of order, violating ACID properties and corrupting data.

⏱️ Time Estimate: 90-120 minutes
📊 Difficulty: Expert

Build a tool that demonstrates memory reordering and the effects of memory barriers. Your visualizer should show how operations can be reordered without synchronization, demonstrate the guarantees provided by different synchronization primitives, and prove that proper synchronization establishes correct memory ordering.

Solution with Explanation
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"runtime"
  7	"sync"
  8	"sync/atomic"
  9	"time"
 10)
 11
 12type MemoryBarrierTest struct {
 13	name        string
 14	iterations  int
 15	reorderings int
 16}
 17
 18// Test 1: No synchronization (may observe reordering)
 19func testNoSync(iterations int) *MemoryBarrierTest {
 20	test := &MemoryBarrierTest{
 21		name:       "No Synchronization",
 22		iterations: iterations,
 23	}
 24
 25	for i := 0; i < iterations; i++ {
 26		var x, y, a, b int
 27
 28		done := make(chan bool, 2)
 29
 30		go func() {
 31			x = 1
 32			a = y
 33			done <- true
 34		}()
 35
 36		go func() {
 37			y = 1
 38			b = x
 39			done <- true
 40		}()
 41
 42		<-done
 43		<-done
 44
 45		// a=0, b=0 shows reordering!
 46		if a == 0 && b == 0 {
 47			test.reorderings++
 48		}
 49	}
 50
 51	return test
 52}
 53
 54// Test 2: With atomic operations (no reordering)
 55func testAtomic(iterations int) *MemoryBarrierTest {
 56	test := &MemoryBarrierTest{
 57		name:       "Atomic Operations",
 58		iterations: iterations,
 59	}
 60
 61	for i := 0; i < iterations; i++ {
 62		var x, y atomic.Int32
 63		var a, b int32
 64
 65		done := make(chan bool, 2)
 66
 67		go func() {
 68			x.Store(1)
 69			a = y.Load()
 70			done <- true
 71		}()
 72
 73		go func() {
 74			y.Store(1)
 75			b = x.Load()
 76			done <- true
 77		}()
 78
 79		<-done
 80		<-done
 81
 82		// With atomic, a=0 and b=0 should NOT occur
 83		if a == 0 && b == 0 {
 84			test.reorderings++
 85		}
 86	}
 87
 88	return test
 89}
 90
 91// Test 3: With mutex (no reordering)
 92func testMutex(iterations int) *MemoryBarrierTest {
 93	test := &MemoryBarrierTest{
 94		name:       "Mutex Synchronization",
 95		iterations: iterations,
 96	}
 97
 98	for i := 0; i < iterations; i++ {
 99		var x, y, a, b int
100		var mu sync.Mutex
101
102		done := make(chan bool, 2)
103
104		go func() {
105			mu.Lock()
106			x = 1
107			mu.Unlock()
108
109			mu.Lock()
110			a = y
111			mu.Unlock()
112
113			done <- true
114		}()
115
116		go func() {
117			mu.Lock()
118			y = 1
119			mu.Unlock()
120
121			mu.Lock()
122			b = x
123			mu.Unlock()
124
125			done <- true
126		}()
127
128		<-done
129		<-done
130
131		// With mutex, a=0 and b=0 should NOT occur
132		if a == 0 && b == 0 {
133			test.reorderings++
134		}
135	}
136
137	return test
138}
139
140// Test 4: With channels (no reordering)
141func testChannels(iterations int) *MemoryBarrierTest {
142	test := &MemoryBarrierTest{
143		name:       "Channel Synchronization",
144		iterations: iterations,
145	}
146
147	for i := 0; i < iterations; i++ {
148		var x, y, a, b int
149		ch1 := make(chan struct{})
150		ch2 := make(chan struct{})
151
152		done := make(chan bool, 2)
153
154		go func() {
155			x = 1
156			close(ch1) // Signal
157			<-ch2      // Wait
158			a = y
159			done <- true
160		}()
161
162		go func() {
163			y = 1
164			close(ch2) // Signal
165			<-ch1      // Wait
166			b = x
167			done <- true
168		}()
169
170		<-done
171		<-done
172
173		// With channels, a=0 and b=0 should NOT occur
174		if a == 0 && b == 0 {
175			test.reorderings++
176		}
177	}
178
179	return test
180}
181
182func main() {
183	fmt.Println("=== Memory Barrier Visualizer ===\n")
184	fmt.Println("Testing memory ordering with different synchronization primitives")
185	fmt.Println("Looking for reordering: a=0, b=0 (impossible with sequential consistency)\n")
186
187	runtime.GOMAXPROCS(runtime.NumCPU())
188
189	iterations := 10000
190
191	// Run tests
192	tests := []*MemoryBarrierTest{
193		testNoSync(iterations),
194		testAtomic(iterations),
195		testMutex(iterations),
196		testChannels(iterations),
197	}
198
199	// Print results
200	fmt.Println("=== Results ===\n")
201	for _, test := range tests {
202		percentage := float64(test.reorderings) / float64(test.iterations) * 100
203		fmt.Printf("%s:\n", test.name)
204		fmt.Printf("  Iterations: %d\n", test.iterations)
205		fmt.Printf("  Reorderings observed: %d (%.2f%%)\n", test.reorderings, percentage)
206
207		if test.reorderings > 0 {
208			fmt.Printf("  ⚠️  Weak memory ordering detected!\n")
209		} else {
210			fmt.Printf("  ✓ Proper synchronization: No reordering\n")
211		}
212		fmt.Println()
213	}
214
215	// Explanation
216	fmt.Println("=== Explanation ===\n")
217	fmt.Println("1. No Synchronization:")
218	fmt.Println("   - Compiler/CPU can reorder operations")
219	fmt.Println("   - a=0, b=0 is possible due to reordering")
220	fmt.Println("   - This is a data race!\n")
221
222	fmt.Println("2. Atomic Operations:")
223	fmt.Println("   - Provide sequential consistency for atomic variables")
224	fmt.Println("   - Prevent reordering across atomic operations")
225	fmt.Println("   - a=0, b=0 cannot occur\n")
226
227	fmt.Println("3. Mutex Synchronization:")
228	fmt.Println("   - Lock provides acquire semantics")
229	fmt.Println("   - Unlock provides release semantics")
230	fmt.Println("   - Establishes happens-before relationships\n")
231
232	fmt.Println("4. Channel Synchronization:")
233	fmt.Println("   - Send/receive operations are synchronization points")
234	fmt.Println("   - Provide happens-before guarantees")
235	fmt.Println("   - Ensure proper memory ordering\n")
236
237	fmt.Println("=== Memory Barrier Types ===\n")
238	fmt.Println("1. Compiler Barrier: Prevents compiler reordering")
239	fmt.Println("2. CPU Barrier: Prevents hardware reordering")
240	fmt.Println("3. Full Barrier: Both compiler + CPU barriers")
241	fmt.Println("   (Go's sync primitives provide full barriers)\n")
242}

Explanation:

This visualizer demonstrates memory reordering:

  1. No Synchronization: Shows that without proper synchronization, operations can be reordered, leading to impossible outcomes in a sequentially consistent model.

  2. Atomic Operations: Demonstrate that atomic operations prevent reordering and provide proper memory ordering.

  3. Mutex Synchronization: Shows that mutex lock/unlock operations establish proper happens-before relationships.

  4. Channel Synchronization: Demonstrates that channel operations provide memory ordering guarantees.

Key Insights:

  • Weak memory models allow reordering for performance
  • Synchronization primitives provide memory barriers
  • Memory barriers ensure proper ordering and visibility
  • Always use synchronization to prevent data races

Exercise 7: Lock-Free Queue Implementation

🎯 Learning Objectives:

  • Implement lock-free data structures using Compare-And-Swap
  • Master atomic operations for concurrent algorithms
  • Understand ABA problem and prevention techniques
  • Learn trade-offs between lock-based and lock-free algorithms

🌍 Real-World Context:
Lock-free data structures are critical for high-performance systems where lock contention becomes a bottleneck. Companies like Microsoft use lock-free queues in their Windows kernel. Intel CPUs provide hardware support for atomic operations that make lock-free algorithms possible. Understanding lock-free programming is essential for building systems that need to handle millions of operations per second with minimal latency.

⏱️ Time Estimate: 120-150 minutes
📊 Difficulty: Expert

Implement a lock-free queue using atomic Compare-And-Swap operations. Your queue should support concurrent enqueue and dequeue operations without locks, handle the ABA problem correctly, provide linearizability guarantees, and demonstrate performance benefits over lock-based implementations.

Solution with Explanation
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"sync"
  7	"sync/atomic"
  8	"time"
  9	"unsafe"
 10)
 11
 12// Node in the lock-free queue
 13type Node struct {
 14	value interface{}
 15	next  unsafe.Pointer // *Node
 16}
 17
 18// LockFreeQueue implements a lock-free queue using CAS
 19type LockFreeQueue struct {
 20	head unsafe.Pointer // *Node
 21	tail unsafe.Pointer // *Node
 22}
 23
 24func NewLockFreeQueue() *LockFreeQueue {
 25	// Create dummy node
 26	dummy := &Node{}
 27	q := &LockFreeQueue{
 28		head: unsafe.Pointer(dummy),
 29		tail: unsafe.Pointer(dummy),
 30	}
 31	return q
 32}
 33
 34// Enqueue adds a value to the queue
 35func (q *LockFreeQueue) Enqueue(value interface{}) {
 36	node := &Node{value: value}
 37
 38	for {
 39		// Load tail and next
 40		tail := (*Node)(atomic.LoadPointer(&q.tail))
 41		next := atomic.LoadPointer(&tail.next)
 42
 43		// Check if tail is still consistent
 44		if tail == (*Node)(atomic.LoadPointer(&q.tail)) {
 45			// Is tail pointing to the last node?
 46			if next == nil {
 47				// Try to link node at the end of the list
 48				if atomic.CompareAndSwapPointer(&tail.next, next, unsafe.Pointer(node)) {
 49					// Enqueue is done, try to swing tail to the inserted node
 50					atomic.CompareAndSwapPointer(&q.tail, unsafe.Pointer(tail), unsafe.Pointer(node))
 51					return
 52				}
 53			} else {
 54				// Tail was not pointing to last node, try to swing tail to next
 55				atomic.CompareAndSwapPointer(&q.tail, unsafe.Pointer(tail), next)
 56			}
 57		}
 58	}
 59}
 60
 61// Dequeue removes and returns a value from the queue
 62func (q *LockFreeQueue) Dequeue() (interface{}, bool) {
 63	for {
 64		// Load head, tail, and next
 65		head := (*Node)(atomic.LoadPointer(&q.head))
 66		tail := (*Node)(atomic.LoadPointer(&q.tail))
 67		next := (*Node)(atomic.LoadPointer(&head.next))
 68
 69		// Check if head is still consistent
 70		if head == (*Node)(atomic.LoadPointer(&q.head)) {
 71			// Is queue empty or tail falling behind?
 72			if head == tail {
 73				// Is queue empty?
 74				if next == nil {
 75					return nil, false
 76				}
 77				// Tail is falling behind, try to advance it
 78				atomic.CompareAndSwapPointer(&q.tail, unsafe.Pointer(tail), unsafe.Pointer(next))
 79			} else {
 80				// Read value before CAS, otherwise another dequeue might free the next node
 81				value := next.value
 82
 83				// Try to swing head to the next node
 84				if atomic.CompareAndSwapPointer(&q.head, unsafe.Pointer(head), unsafe.Pointer(next)) {
 85					return value, true
 86				}
 87			}
 88		}
 89	}
 90}
 91
 92// IsEmpty checks if the queue is empty
 93func (q *LockFreeQueue) IsEmpty() bool {
 94	head := (*Node)(atomic.LoadPointer(&q.head))
 95	next := atomic.LoadPointer(&head.next)
 96	return next == nil
 97}
 98
 99// Lock-based queue for comparison
100type LockQueue struct {
101	mu    sync.Mutex
102	items []interface{}
103}
104
105func NewLockQueue() *LockQueue {
106	return &LockQueue{
107		items: make([]interface{}, 0),
108	}
109}
110
111func (q *LockQueue) Enqueue(value interface{}) {
112	q.mu.Lock()
113	q.items = append(q.items, value)
114	q.mu.Unlock()
115}
116
117func (q *LockQueue) Dequeue() (interface{}, bool) {
118	q.mu.Lock()
119	defer q.mu.Unlock()
120
121	if len(q.items) == 0 {
122		return nil, false
123	}
124
125	value := q.items[0]
126	q.items = q.items[1:]
127	return value, true
128}
129
130// Benchmark helper
131func benchmarkQueue(name string, enqueue func(interface{}), dequeue func() (interface{}, bool)) {
132	start := time.Now()
133
134	const operations = 100000
135	const goroutines = 10
136
137	var wg sync.WaitGroup
138
139	// Producers
140	for i := 0; i < goroutines; i++ {
141		wg.Add(1)
142		go func(id int) {
143			defer wg.Done()
144			for j := 0; j < operations/goroutines; j++ {
145				enqueue(id*1000 + j)
146			}
147		}(i)
148	}
149
150	// Consumers
151	for i := 0; i < goroutines; i++ {
152		wg.Add(1)
153		go func() {
154			defer wg.Done()
155			for j := 0; j < operations/goroutines; j++ {
156				for {
157					if _, ok := dequeue(); ok {
158						break
159					}
160					time.Sleep(time.Microsecond)
161				}
162			}
163		}()
164	}
165
166	wg.Wait()
167
168	duration := time.Since(start)
169	fmt.Printf("%s:\n", name)
170	fmt.Printf("  Operations: %d\n", operations*2)
171	fmt.Printf("  Duration: %v\n", duration)
172	fmt.Printf("  Ops/sec: %.0f\n\n", float64(operations*2)/duration.Seconds())
173}
174
175func main() {
176	fmt.Println("=== Lock-Free Queue Implementation ===\n")
177
178	// Test correctness
179	fmt.Println("Testing correctness:")
180	lfq := NewLockFreeQueue()
181
182	// Enqueue
183	for i := 1; i <= 5; i++ {
184		lfq.Enqueue(i)
185		fmt.Printf("Enqueued: %d\n", i)
186	}
187
188	// Dequeue
189	fmt.Println("\nDequeuing:")
190	for !lfq.IsEmpty() {
191		if val, ok := lfq.Dequeue(); ok {
192			fmt.Printf("Dequeued: %d\n", val)
193		}
194	}
195
196	// Test with concurrent operations
197	fmt.Println("\n=== Concurrent Operations Test ===\n")
198
199	lfq = NewLockFreeQueue()
200	var wg sync.WaitGroup
201
202	// Multiple producers
203	for i := 0; i < 3; i++ {
204		wg.Add(1)
205		go func(id int) {
206			defer wg.Done()
207			for j := 0; j < 10; j++ {
208				lfq.Enqueue(id*100 + j)
209			}
210		}(i)
211	}
212
213	// Multiple consumers
214	results := make([]interface{}, 0, 30)
215	var mu sync.Mutex
216
217	for i := 0; i < 3; i++ {
218		wg.Add(1)
219		go func() {
220			defer wg.Done()
221			for j := 0; j < 10; j++ {
222				for {
223					if val, ok := lfq.Dequeue(); ok {
224						mu.Lock()
225						results = append(results, val)
226						mu.Unlock()
227						break
228					}
229					time.Sleep(time.Microsecond)
230				}
231			}
232		}()
233	}
234
235	wg.Wait()
236
237	fmt.Printf("Enqueued: 30 items\n")
238	fmt.Printf("Dequeued: %d items\n", len(results))
239	fmt.Printf("All items processed: %v\n\n", len(results) == 30)
240
241	// Performance comparison
242	fmt.Println("=== Performance Comparison ===\n")
243
244	// Lock-free queue
245	lfq = NewLockFreeQueue()
246	benchmarkQueue("Lock-Free Queue", lfq.Enqueue, lfq.Dequeue)
247
248	// Lock-based queue
249	lq := NewLockQueue()
250	benchmarkQueue("Lock-Based Queue", lq.Enqueue, lq.Dequeue)
251
252	// Explanation
253	fmt.Println("=== Key Concepts ===\n")
254	fmt.Println("1. Compare-And-Swap (CAS):")
255	fmt.Println("   - Atomic operation that updates value only if it matches expected")
256	fmt.Println("   - Provides lock-free synchronization")
257	fmt.Println("   - Retry loop handles contention\n")
258
259	fmt.Println("2. Lock-Free Progress:")
260	fmt.Println("   - At least one thread makes progress")
261	fmt.Println("   - No thread can block others indefinitely")
262	fmt.Println("   - Better scalability under high contention\n")
263
264	fmt.Println("3. Memory Ordering:")
265	fmt.Println("   - Atomic operations provide memory barriers")
266	fmt.Println("   - Ensures visibility across threads")
267	fmt.Println("   - No data races despite lack of locks\n")
268
269	fmt.Println("4. ABA Problem:")
270	fmt.Println("   - Value changes from A to B and back to A")
271	fmt.Println("   - CAS thinks nothing changed")
272	fmt.Println("   - Solution: Version numbers or hazard pointers\n")
273}

Explanation:

This lock-free queue implementation demonstrates:

  1. CAS Operations: Uses Compare-And-Swap for lock-free synchronization. Multiple threads can attempt operations simultaneously, and CAS ensures only one succeeds atomically.

  2. Michael-Scott Algorithm: Industry-standard lock-free queue algorithm that uses a dummy node to simplify edge cases.

  3. Memory Ordering: Atomic operations provide necessary memory barriers to ensure proper visibility across threads.

  4. Performance: Lock-free algorithms often outperform lock-based ones under high contention because they avoid lock overhead and allow more parallelism.

Advantages:

  • No lock contention
  • Better scalability
  • No deadlock possible
  • Predictable latency

Disadvantages:

  • More complex to implement correctly
  • Retry loops consume CPU
  • May have lower throughput under low contention
  • Requires atomic operation support

Exercise 8: Deadlock Detector

🎯 Learning Objectives:

  • Implement deadlock detection algorithms for concurrent programs
  • Master cycle detection in wait-for graphs
  • Learn to identify circular dependencies in lock acquisition
  • Practice systematic debugging of deadlock scenarios

🌍 Real-World Context:
Deadlocks are among the most challenging bugs to detect and fix in concurrent systems. In database systems, deadlock detection is essential for ensuring progress. Commercial databases like PostgreSQL and MySQL implement sophisticated deadlock detection algorithms. Understanding deadlock detection helps you design systems that avoid deadlocks or handle them gracefully when they occur.

⏱️ Time Estimate: 120-150 minutes
📊 Difficulty: Expert

Build a deadlock detector that monitors lock acquisition patterns and detects potential deadlocks in concurrent programs. Your detector should track lock acquisitions, build a wait-for graph, detect cycles indicating deadlocks, and provide actionable feedback on how to resolve the deadlocks.

Solution with Explanation
  1// run
  2package main
  3
  4import (
  5	"fmt"
  6	"sync"
  7	"time"
  8)
  9
 10// LockID uniquely identifies a lock
 11type LockID int
 12
 13// GoroutineID uniquely identifies a goroutine
 14type GoroutineID int
 15
 16// LockEvent represents a lock operation
 17type LockEvent struct {
 18	Goroutine GoroutineID
 19	Lock      LockID
 20	Operation string // "acquire", "release", "waiting"
 21	Timestamp time.Time
 22}
 23
 24// DeadlockDetector monitors lock operations
 25type DeadlockDetector struct {
 26	mu sync.Mutex
 27
 28	// Current lock holders: lockID -> goroutineID
 29	holders map[LockID]GoroutineID
 30
 31	// Waiting goroutines: goroutineID -> lockID
 32	waiting map[GoroutineID]LockID
 33
 34	// Events history
 35	events []LockEvent
 36
 37	// Detected deadlocks
 38	deadlocks [][]GoroutineID
 39}
 40
 41func NewDeadlockDetector() *DeadlockDetector {
 42	return &DeadlockDetector{
 43		holders:   make(map[LockID]GoroutineID),
 44		waiting:   make(map[GoroutineID]LockID),
 45		events:    make([]LockEvent, 0),
 46		deadlocks: make([][]GoroutineID, 0),
 47	}
 48}
 49
 50// RecordAcquire records a successful lock acquisition
 51func (dd *DeadlockDetector) RecordAcquire(goroutine GoroutineID, lock LockID) {
 52	dd.mu.Lock()
 53	defer dd.mu.Unlock()
 54
 55	dd.holders[lock] = goroutine
 56	delete(dd.waiting, goroutine)
 57
 58	dd.events = append(dd.events, LockEvent{
 59		Goroutine: goroutine,
 60		Lock:      lock,
 61		Operation: "acquire",
 62		Timestamp: time.Now(),
 63	})
 64}
 65
 66// RecordRelease records a lock release
 67func (dd *DeadlockDetector) RecordRelease(goroutine GoroutineID, lock LockID) {
 68	dd.mu.Lock()
 69	defer dd.mu.Unlock()
 70
 71	delete(dd.holders, lock)
 72
 73	dd.events = append(dd.events, LockEvent{
 74		Goroutine: goroutine,
 75		Lock:      lock,
 76		Operation: "release",
 77		Timestamp: time.Now(),
 78	})
 79}
 80
 81// RecordWaiting records a goroutine waiting for a lock
 82func (dd *DeadlockDetector) RecordWaiting(goroutine GoroutineID, lock LockID) {
 83	dd.mu.Lock()
 84	defer dd.mu.Unlock()
 85
 86	dd.waiting[goroutine] = lock
 87
 88	dd.events = append(dd.events, LockEvent{
 89		Goroutine: goroutine,
 90		Lock:      lock,
 91		Operation: "waiting",
 92		Timestamp: time.Now(),
 93	})
 94
 95	// Check for deadlock
 96	if cycle := dd.detectCycle(); len(cycle) > 0 {
 97		dd.deadlocks = append(dd.deadlocks, cycle)
 98		fmt.Printf("\n⚠️  DEADLOCK DETECTED: %v\n\n", cycle)
 99	}
100}
101
102// detectCycle detects cycles in the wait-for graph
103func (dd *DeadlockDetector) detectCycle() []GoroutineID {
104	// Build wait-for graph: goroutine -> goroutine
105	graph := make(map[GoroutineID]GoroutineID)
106
107	for waitingG, wantedLock := range dd.waiting {
108		if holder, ok := dd.holders[wantedLock]; ok {
109			graph[waitingG] = holder
110		}
111	}
112
113	// Detect cycle using DFS
114	visited := make(map[GoroutineID]bool)
115	recStack := make(map[GoroutineID]bool)
116	path := make([]GoroutineID, 0)
117
118	var dfs func(GoroutineID) []GoroutineID
119	dfs = func(g GoroutineID) []GoroutineID {
120		visited[g] = true
121		recStack[g] = true
122		path = append(path, g)
123
124		if next, ok := graph[g]; ok {
125			if !visited[next] {
126				if cycle := dfs(next); cycle != nil {
127					return cycle
128				}
129			} else if recStack[next] {
130				// Found cycle
131				cycleStart := 0
132				for i, v := range path {
133					if v == next {
134						cycleStart = i
135						break
136					}
137				}
138				return path[cycleStart:]
139			}
140		}
141
142		recStack[g] = false
143		path = path[:len(path)-1]
144		return nil
145	}
146
147	for g := range dd.waiting {
148		if !visited[g] {
149			if cycle := dfs(g); cycle != nil {
150				return cycle
151			}
152		}
153	}
154
155	return nil
156}
157
158// Report generates a deadlock report
159func (dd *DeadlockDetector) Report() {
160	dd.mu.Lock()
161	defer dd.mu.Unlock()
162
163	fmt.Println("\n=== Deadlock Detection Report ===\n")
164
165	if len(dd.deadlocks) == 0 {
166		fmt.Println("No deadlocks detected ✓")
167		return
168	}
169
170	fmt.Printf("Detected %d deadlock(s):\n\n", len(dd.deadlocks))
171
172	for i, cycle := range dd.deadlocks {
173		fmt.Printf("Deadlock %d:\n", i+1)
174		fmt.Printf("  Cycle: %v\n", cycle)
175
176		// Show what each goroutine is waiting for
177		for _, g := range cycle {
178			if lock, ok := dd.waiting[g]; ok {
179				holder := dd.holders[lock]
180				fmt.Printf("  G%d waiting for Lock%d (held by G%d)\n", g, lock, holder)
181			}
182		}
183		fmt.Println()
184	}
185
186	// Show event timeline
187	fmt.Println("Event Timeline:")
188	for _, event := range dd.events {
189		fmt.Printf("  [%s] G%d %s Lock%d\n",
190			event.Timestamp.Format("15:04:05.000"),
191			event.Goroutine, event.Operation, event.Lock)
192	}
193}
194
195// MonitoredMutex wraps sync.Mutex with deadlock detection
196type MonitoredMutex struct {
197	id       LockID
198	mu       sync.Mutex
199	detector *DeadlockDetector
200}
201
202func NewMonitoredMutex(id LockID, detector *DeadlockDetector) *MonitoredMutex {
203	return &MonitoredMutex{
204		id:       id,
205		detector: detector,
206	}
207}
208
209func (m *MonitoredMutex) Lock(goroutine GoroutineID) {
210	m.detector.RecordWaiting(goroutine, m.id)
211	m.mu.Lock()
212	m.detector.RecordAcquire(goroutine, m.id)
213}
214
215func (m *MonitoredMutex) Unlock(goroutine GoroutineID) {
216	m.mu.Unlock()
217	m.detector.RecordRelease(goroutine, m.id)
218}
219
220func main() {
221	fmt.Println("=== Deadlock Detector ===\n")
222
223	detector := NewDeadlockDetector()
224
225	// Create two locks
226	lock1 := NewMonitoredMutex(1, detector)
227	lock2 := NewMonitoredMutex(2, detector)
228
229	// Scenario 1: No deadlock (correct lock ordering)
230	fmt.Println("Scenario 1: Correct Lock Ordering (No Deadlock)")
231	var wg sync.WaitGroup
232
233	wg.Add(2)
234
235	// Goroutine 1: Lock1 -> Lock2
236	go func() {
237		defer wg.Done()
238		lock1.Lock(1)
239		time.Sleep(100 * time.Millisecond)
240		lock2.Lock(1)
241		fmt.Println("G1: Got both locks")
242		lock2.Unlock(1)
243		lock1.Unlock(1)
244	}()
245
246	// Goroutine 2: Lock1 -> Lock2 (same order)
247	go func() {
248		defer wg.Done()
249		time.Sleep(50 * time.Millisecond)
250		lock1.Lock(2)
251		lock2.Lock(2)
252		fmt.Println("G2: Got both locks")
253		lock2.Unlock(2)
254		lock1.Unlock(2)
255	}()
256
257	wg.Wait()
258	time.Sleep(200 * time.Millisecond)
259
260	detector.Report()
261
262	// Scenario 2: Deadlock (incorrect lock ordering)
263	fmt.Println("\n\nScenario 2: Incorrect Lock Ordering (Deadlock)")
264
265	detector = NewDeadlockDetector()
266	lock1 = NewMonitoredMutex(1, detector)
267	lock2 = NewMonitoredMutex(2, detector)
268
269	wg.Add(2)
270
271	// Goroutine 3: Lock1 -> Lock2
272	go func() {
273		defer wg.Done()
274		lock1.Lock(3)
275		fmt.Println("G3: Acquired Lock1")
276		time.Sleep(100 * time.Millisecond)
277
278		fmt.Println("G3: Trying to acquire Lock2...")
279		lock2.Lock(3)
280		fmt.Println("G3: Acquired Lock2")
281
282		lock2.Unlock(3)
283		lock1.Unlock(3)
284	}()
285
286	// Goroutine 4: Lock2 -> Lock1 (reverse order!)
287	go func() {
288		defer wg.Done()
289		time.Sleep(50 * time.Millisecond)
290
291		lock2.Lock(4)
292		fmt.Println("G4: Acquired Lock2")
293		time.Sleep(100 * time.Millisecond)
294
295		fmt.Println("G4: Trying to acquire Lock1...")
296		lock1.Lock(4)
297		fmt.Println("G4: Acquired Lock1")
298
299		lock1.Unlock(4)
300		lock2.Unlock(4)
301	}()
302
303	// Give time for deadlock to form
304	time.Sleep(500 * time.Millisecond)
305
306	detector.Report()
307
308	fmt.Println("\n=== Deadlock Prevention Strategies ===\n")
309	fmt.Println("1. Lock Ordering:")
310	fmt.Println("   - Always acquire locks in the same order")
311	fmt.Println("   - Assign global ordering to all locks")
312	fmt.Println("   - Example: Always Lock1 before Lock2\n")
313
314	fmt.Println("2. Lock Timeout:")
315	fmt.Println("   - Use TryLock with timeout")
316	fmt.Println("   - Release all locks if can't acquire")
317	fmt.Println("   - Retry with backoff\n")
318
319	fmt.Println("3. Lock Hierarchy:")
320	fmt.Println("   - Define levels for locks")
321	fmt.Println("   - Only acquire locks at higher levels")
322	fmt.Println("   - Prevents circular dependencies\n")
323
324	fmt.Println("4. Deadlock Detection:")
325	fmt.Println("   - Monitor lock acquisitions")
326	fmt.Println("   - Build wait-for graph")
327	fmt.Println("   - Detect cycles")
328	fmt.Println("   - Abort one transaction to break cycle\n")
329}

Explanation:

This deadlock detector demonstrates:

  1. Wait-For Graph: Tracks which goroutines are waiting for locks held by other goroutines. A deadlock exists when there's a cycle in this graph.

  2. Cycle Detection: Uses depth-first search (DFS) to detect cycles in the wait-for graph. A cycle means goroutines are waiting in a circular pattern.

  3. Event Timeline: Records all lock operations with timestamps, enabling post-mortem analysis of deadlocks.

  4. Deadlock Prevention: The example shows how proper lock ordering prevents deadlocks, while incorrect ordering causes them.

Key Insights:

Coffman Conditions (all must be true for deadlock):

  1. Mutual Exclusion: Resources can't be shared
  2. Hold and Wait: Hold locks while waiting for others
  3. No Preemption: Locks can't be forcibly taken
  4. Circular Wait: Cycle in wait-for graph

Prevention Strategies:

  1. Lock Ordering: Break circular wait
  2. Timeouts: Break hold and wait
  3. Try-Lock: Allow backoff and retry
  4. Lock Hierarchy: Enforce ordering discipline

Summary

💡 Key Takeaway: The Go memory model is not about theory—it's about writing correct concurrent programs that work reliably in production. Understanding happens-before relationships is essential for preventing subtle bugs that can cost millions.

The Go memory model ensures predictable concurrent behavior:

  • Happens-Before: Defines when memory operations are visible across goroutines
  • Synchronization: Use mutexes, channels, or atomics for safe concurrent access
  • Memory Ordering: Go doesn't guarantee sequential consistency without synchronization
  • Race Prevention: Understand common patterns and use -race detector
  • Production Patterns: Message passing, RCU, and proper synchronization

When to Use What: Synchronization Cheat Sheet

🎯 Practical Guide: Choose the right tool for your concurrent programming needs:

Scenario Best Choice Why
Simple counter sync/atomic Fastest for single variables, no locks
Complex data structures sync.Mutex Protects multiple variables as a unit
Read-heavy operations sync.RWMutex Multiple readers, single writer optimization
Coordination between goroutines Channels Go's preferred approach, both communication and sync
One-time initialization sync.Once Guaranteed single execution, thread-safe
Wait for multiple goroutines sync.WaitGroup Clean shutdown patterns
Condition variables sync.Cond Wait for specific conditions
Lock-free algorithms atomic.Value High-performance, no contention

Common Pitfalls to Avoid

⚠️ Important: These patterns cause production bugs:

  1. Maps without protection - Always use mutexes with concurrent map access
  2. Loop variable capture - Pass values as parameters to goroutines
  3. Assuming sequential consistency - Use synchronization, not assumptions
  4. Forgetting the race detector - Run tests with go test -race
  5. Incorrect lock ordering - Always acquire locks in the same order
  6. Missing synchronization - Don't assume writes are visible without synchronization

Real-world Impact: These patterns have caused outages at major companies including Cloudflare, Uber, and others. The cost of a single data race can range from thousands to millions of dollars.

Best Practices Checklist

  • Always run with race detector in development: go run -race
  • Prefer channels for goroutine communication: Go's philosophy
  • Use mutexes for protecting shared state: Simple and effective
  • Consider atomics for simple counters: Better performance
  • Never share mutable state without synchronization: Root cause of races
  • Test under realistic load: Races often appear only under stress
  • Use proper lock ordering: Prevent deadlocks
  • Document synchronization patterns: Help future maintainers

Production Tip: Set up CI pipelines that run with -race enabled. It's better to catch races in development than in production at 3 AM.

Memory Model Rules:

  1. Within a goroutine: program order
  2. Channel send happens-before receive completes
  3. Mutex unlock happens-before subsequent lock
  4. sync.Once.Do() completes before any Do() returns
  5. Goroutine creation happens-before goroutine execution
  6. Channel close happens-before receive of zero value
  7. For buffered channels, Kth send happens-before (K+C)th receive completes
  8. sync.WaitGroup.Done() happens-before corresponding Wait() returns

Final Thought: Understanding the memory model transforms concurrent programming from guesswork into engineering. You're not just writing code—you're specifying exactly when and how different parts of your program will see each other's changes.