Stage 1 · Code
Concurrency in Practice
Mutexes & WaitGroups
Channels are great for handoffs, but some concurrent problems are really about shared state or simple completion tracking. This lesson shows the synchronization tools Go gives you for those cases.
When Shared State Needs Protection
Channels shine when ownership of a value can move from one goroutine to another. But not every problem looks like a handoff. Sometimes many goroutines really do need the same shared thing: a counter, a cache, a map of active users, or an in-memory statistics structure. In those cases, the question is no longer who should receive this value next? The question is how do we stop several goroutines from stepping on the same memory at the same time?
A shared counter is the simplest honest example. counter++ looks like one tiny action. Under the hood it is three steps: read the old value, compute the new value, write the new value back. If two goroutines do that at the same time, both may read the same old value before either write happens. One update gets lost.
A WaitGroup answers are the goroutines finished yet? It does not answer did they update shared state safely? Those are different problems, and they need different tools.
Mutex and RWMutex
A mutex is the simplest answer to the shared-state problem. The word means mutual exclusion. When one goroutine locks the mutex, other goroutines trying to lock the same mutex must wait. That gives one goroutine exclusive access to the protected data for a short period.
In practice you will usually write the lock and unlock as tightly as possible. Lock, do the minimum protected work, unlock. The longer you hold a mutex, the more you slow down every other goroutine that needs that same data.
Go also gives you sync.RWMutex, which separates readers from writers. Multiple readers may hold the read lock at the same time with RLock, but a writer using Lock still gets exclusive access. That makes sense when reads are very frequent, writes are less frequent, and the protected work is large enough that allowing concurrent readers is worth the extra complexity.
WaitGroups and Launch Order
The other common synchronization problem is much simpler: maybe goroutines do not need to send results back at all. You just need to know when they are all finished. That is exactly what sync.WaitGroup is for.
There is one beginner bug worth calling out explicitly because it looks harmless and sometimes even appears to work: calling Add inside the goroutine instead of before launching it. The problem is a race between main calling Wait and the goroutine getting scheduled late.
The WaitGroup counter should describe work that is already promised to happen. Calling Add first is like writing three names on a whiteboard before three people leave the room. Calling Add inside the goroutine is like hoping they remember to sign in after they have already left.
Quiz and Practice
Why is `counter++` unsafe when multiple goroutines do it at the same time?
Hands-On Project
Practice both primitives together: launch multiple goroutines, have each one increment shared state several times, and return the correct final total every run.
Write a function `SafeTotal(workers, increments int) int` that launches `workers` goroutines. Each goroutine should increment one shared counter `increments` times. Use a `sync.Mutex` to protect the counter and a `sync.WaitGroup` to wait for all goroutines. Return the final counter value.
Summary and Key Takeaways
- Use a mutex when multiple goroutines truly need the same mutable state.
- A mutex protects a critical section; it does not make work parallel or faster by itself.
- Reach for
RWMutexwhen reads clearly dominate and concurrent readers are genuinely useful. - Use
WaitGroupwhen you need to know when N goroutines are finished, even if they send no data back. - Call
wg.Add(...)before launching goroutines, not inside them.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.