Stage 1 · Code
Concurrency in Practice
Choosing the Right Pattern
Concurrency gets much easier when you stop asking for one magic tool and start asking a better question: what exact coordination problem am I solving here?
Channels or Mutexes
A lot of beginner confusion comes from treating concurrency tools like interchangeable gadgets. They are not. Channels and mutexes solve different shapes of problems. If you start with the problem shape, the choice gets much simpler.
| Question you are really asking | Usually the natural tool | Why |
|---|---|---|
| How does one goroutine hand work or data to another? | Channels | The handoff itself is the coordination point |
| How do several goroutines safely use one shared mutable value? | Mutex or RWMutex | The value stays shared, so access must be protected |
| How do I know when N goroutines have all finished? | WaitGroup | This is a completion problem, not a data-transfer problem |
A helpful rule of thumb is Go's old phrase: share memory by communicating. If ownership can move cleanly from one goroutine to another, channels usually feel natural. If the data truly remains shared, a mutex is usually the honest tool. Trying to force every shared-state problem into channels can make code more tangled, not more idiomatic.
Done Channel or Context
The same kind of choice appears with cancellation. Sometimes a small local done channel is enough. Sometimes you need a full context. The difference is scope and richness.
- Use a done channel when the cancellation signal is very local, usually inside one package or one small concurrent component, and all you need is
stop now. - Use
context.Contextwhen the signal must travel across function boundaries, API layers, goroutine trees, or network and database calls. - Use context when you also need deadlines, timeouts, or request-scoped metadata in addition to cancellation.
Common Bugs and Red Flags
Most concurrency bugs are not mysterious once you learn the warning signs. They usually come from a goroutine waiting forever, a channel send with no matching receiver, a receive that can never complete, or shared memory being touched without protection.
go test -race ./...You learned the race detector earlier in the course. Keep using it. Timing-dependent shared-memory bugs are exactly the kind of problem humans miss during casual testing and the race detector can expose.
Sometimes an infinite wait is intentional, like a long-lived server loop. But most leaked goroutines come from a missing cancellation path, a missing close, or a channel operation that no other goroutine can ever match. Ask yourself what event lets this goroutine stop.
Review Checklist
When you review concurrent Go code, do not just ask whether it compiles. Walk through a short checklist. Good concurrency code is not only about the happy path. It is about how the edges end.
- What exact problem is each tool solving here: data handoff, shared-state protection, completion tracking, or cancellation?
- Can every goroutine eventually stop, or is any goroutine able to block forever with no escape path?
- For every channel send, who can receive it? For every receive, who can send it?
- If a channel is ranged over, who owns closing it, and when does that happen?
- If shared mutable state exists, is every access consistently protected by the same mutex or other synchronization rule?
- Does cancellation need a local done channel or a context that travels through a wider call chain?
- Has the code been exercised with
go test -racewhen shared memory is involved?
Quiz and Practice
When is a mutex usually more natural than a channel?
Hands-On Project
This final practice exercise is small, but it checks whether you can write responsive code that pays attention to both useful data and cancellation.
Write a function `FirstMessageOrCancel(done <-chan struct{}, data <-chan string) string` that returns the first message received from `data`. If `done` is closed before a message arrives, return `cancelled` instead.
Summary and Key Takeaways
- Choose concurrency tools by problem shape, not by habit.
- Channels are natural for handoff; mutexes are natural for protecting shared mutable state.
- A done channel is fine for local cancellation, while context is better for wider call chains and deadlines.
- Watch for leaks, deadlocks, missing channel closes, and unsynchronized shared memory.
- Review concurrent code by tracing how work starts, how data moves, and how every path ends.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.