In Chapter 14, we gave every goroutine its own slot so no two ever touched the same memory. Now we handle the scenario we deliberately dodged: many goroutines trying to update the exact same variable at once.
Sooner or later, goroutines need to share state. The moment two of them write to the same variable without coordination, Go's safety guarantees fall apart. This chapter shows you what that failure looks like, how a sync.Mutex prevents it, and how to catch these bugs automatically before they reach production.

This chapter covers three concepts:
sync.Mutex: a lock that lets only one goroutine into a section of code at a time.Check out the finish branch to follow along:
bashgit checkout 15-mutexes-and-race-detector-finish
A data race happens when two goroutines access the same piece of memory at the same time, at least one of them is writing, and nothing keeps them in order. When all three conditions are met, the result is undefined. You might get a corrupted value, a crash, or output that is silently wrong.
The trap is that a data race does not always blow up. Sometimes it produces the right answer. Sometimes it is off by a little, and sometimes it takes the whole process down. This inconsistency makes races incredibly hard to find by hand. A test might pass ten times and fail on the eleventh, usually on a different machine or under heavy load.
To see why, imagine a counter shared by many goroutines. Each one runs value++. That single line looks like one atomic step, but it is actually three: read the current value, add one, and write the result back. Picture two goroutines running this at the exact same moment:
value and sees 7.value and also sees 7.8.8.Two increments happened, but the counter only moved by one. An update was lost because both goroutines read the same starting number. If you run 100 goroutines like this, the final total will land somewhere below 100, and the exact number will vary from run to run. There is no bug in the arithmetic. The bug is that nothing stopped the two goroutines from overlapping.
The fix is to ensure only one goroutine runs the read-modify-write cycle at a time. A mutex (short for mutual exclusion) is a lock that does exactly this. One goroutine calls Lock and enters the protected section of code. Any other goroutine that calls Lock must wait until the first one calls Unlock. Only one gets through at a time, so the overlap that loses updates can no longer happen.
Here is SafeCounter from concurrency.go. It bundles an integer value together with the mutex that guards it:
gotype SafeCounter struct {mu sync.Mutexvalue int}func (c *SafeCounter) Inc() {c.mu.Lock()defer c.mu.Unlock()c.value++}func (c *SafeCounter) Value() int {c.mu.Lock()defer c.mu.Unlock()return c.value}
Inc locks the mutex, increments the value, and unlocks it. Because the lock wraps the entire value++ operation, no other goroutine can read or write value while one is in the middle of updating it. The read-modify-write now happens as one indivisible step. No increments are lost.
Using defer c.mu.Unlock() is an important habit. Putting the unlock in a defer guarantees it runs when the method returns, no matter how it returns. If you unlock by hand at the end of a function, and that function returns early or panics before reaching the unlock line, the lock is never released. Every other goroutine will wait for it forever. This is called a deadlock. Using defer removes the chance of forgetting.
Notice that Value locks too, even though it only reads. Reading is still an access. If one goroutine reads value while another writes to it, you have a data race. The rule is simple: every goroutine that touches the shared field, whether reading or writing, must hold the lock first.
Common confusion: A mutex does not belong to any specific method, nor does it magically protect the variable
valueby name. It simply protects whatever code runs betweenLockandUnlock. Keeping thesync.Mutexfield right next to the data it guards, asSafeCounterdoes, is a Go convention. It makes it obvious which lock covers which state.
The mutexCounterDemo function fires 100 goroutines at a single SafeCounter. It uses a sync.WaitGroup from Chapter 14 to wait for all of them to finish:
gofunc mutexCounterDemo() {fmt.Println("\n-- Mutex-protected counter --")const goroutines = 100counter := &SafeCounter{}var wg sync.WaitGroupfor i := 0; i < goroutines; i++ {wg.Add(1)go func() {defer wg.Done()counter.Inc()}()}wg.Wait()fmt.Printf(" %d goroutines each +1 -> final total: %d\n", goroutines, counter.Value())}
Each goroutine calls Inc once, meaning 100 increments happen concurrently. Because every single one goes through the lock, the total is always exactly 100:
text-- Mutex-protected counter --100 goroutines each +1 -> final total: 100
If you took the lock away, this same loop would print a different number on almost every run. It would usually be less than 100, and it would never be predictable. The mutex is the only reason the answer is stable.
You will not always spot a missing lock just by reading code, and a race can hide for a long time before it misbehaves. Go ships with a tool that finds these bugs for you: the race detector. When you add the -race flag, Go instruments your program to watch every memory access at runtime and report any two that conflict.
You turn it on when you run or test your code:
bashgo run -race .go test -race ./...
If the program has a data race, the detector prints a DATA RACE report. It is worth learning to read these reports because they point straight to the problem.

The report pairs up the two accesses that conflicted. Each block tells you what happened (a read or a write), the memory address, which goroutine did it, and the exact file and line number. Once you see two goroutines touching the same address with at least one write, you have found your race. You now know exactly where to add a lock. The finish branch is clean under -race. Because SafeCounter locks every access, the detector has nothing to report.
The detector adds performance overhead, so you usually would not ship a production binary built with -race. Instead, you run it during development and in your Continuous Integration (CI) pipeline. This way, every push runs the test suite under -race, and a new race fails the build instead of reaching your users. Make using it a habit.
This is not a toy pattern. The in-memory task store you will build in Chapter 22 wraps a map[int]Task with a sync.Mutex. It locks the map on every read and every write, exactly like SafeCounter. This is what keeps the store correct once your server is live. Because Go's HTTP server runs each incoming request in its own goroutine, two requests creating tasks at the exact same moment are just two goroutines writing to the same map. This is precisely the situation a mutex is built for.
Shared maps are a classic beginner trap, and they are much less forgiving than a plain counter. When two goroutines write to a Go map at the same time, the runtime usually detects it and aborts the entire program with a fatal concurrent map writes error, rather than quietly corrupting the data. Placing a lock around every map access keeps this from happening.
This closes the Go language foundations: you now have the types, structs, interfaces, errors, and concurrency tools to build real programs. Section 3 builds the HTTP server, where per-request goroutines make everything you just learned about shared state matter in practice.