In 2015 I wrote an article about what I liked about Go, and one of the things I found hardest to explain back then was concurrency. Goroutines looked like threads, but they weren’t. They ran “independently”, but not necessarily in parallel. It all sounded a bit odd until you sat down and wrote some code.
Eleven years later, Anton Zhiyanov has published exactly the resource I would have liked to have that afternoon: Go Concurrency Distilled, a free mini-book that covers all of Go’s concurrency on a single page, with interactive examples you can edit and run in the browser.
What it is (and what it isn’t)
The author makes it clear from the start: it’s a quick refresher, not a beginner’s guide. If you want to learn concurrency from scratch with exercises, he points to his other book, Gist of Go: Concurrency. This one is more like the cheat sheet you keep open in a tab when you haven’t touched select for a while and can’t remember exactly what happens when you read from a closed channel.
And it covers practically everything:
Goroutines · Channels · Select · Pipelines · Time · Context · Wait groups · Data races · Race conditions · Mutexes · Semaphores · Signaling · Once · Pools · Atomics · Testing · Scheduling · Diagnostics
Each topic gets straight to the point: a couple of paragraphs of explanation, an example you can run and, when needed, the trap everyone tends to fall into.
One detail I liked: the book presents itself as AI-free, written without AI help. At a time when it’s hard to tell which documentation was written by someone who actually knows the subject, that’s a statement of intent.
What I liked most
It’s up to date
It uses modern Go APIs, not the ones from five years ago. For example, WaitGroup.Go, which combines the Add, launching the goroutine and the Done in a single call:
var wg sync.WaitGroup
for range 10 {
wg.Go(func() {
fmt.Print(".")
})
}
wg.Wait()
If you’ve been writing Go for years, you’ll recognise what disappears: the wg.Add(1) before each goroutine and the defer wg.Done() inside it, which was easy to forget. It fits nicely with what I wrote a couple of days ago about go fix and modernising code: the language keeps evolving, and learning resources should too.
It also explains synctest, the package for testing concurrent code with a fake clock and without time.Sleep scattered across your tests. It’s one of those things that, once you discover it, makes you wonder how you lived without it.
It separates data races from race conditions
It’s a distinction many people blur, and the book explains it with a very clear example.
A data race happens when several goroutines access the same data and at least one of them modifies it. Go has a tool to detect them: go run -race.
A race condition is something else: the individual operations may be safe, but the order in which they happen leaves the system in an incorrect state. The book’s example is a balance of 50 and two withdrawals of 40 in parallel: both check there’s enough balance before either one deducts, and the balance ends up at -30.
The interesting part is that the race detector finds nothing in that case, because technically there’s no data race. The fix is to protect the whole operation (check and deduct) with a mutex. It’s a good reminder that -race helps enormously, but it doesn’t think for you.
The usual patterns, well explained
The pipelines section is probably the most useful for day-to-day work: output channels, a done channel to signal completion, a cancel channel and three ways to handle errors in a concurrent pipeline (stop at the first error, return a Result type with the value and the error, or collect errors separately). These are patterns you end up reinventing if you don’t know them, and seeing them side by side helps you pick the right one.
The same goes for the lesser-known primitives: the semaphore built with a buffered channel, the rendezvous and the barrier built with a WaitGroup, or sync.Cond for signalling events between goroutines.
What I take away
Go still has what I liked in 2015: a concurrency model that, once you understand it, is surprisingly simple. But “simple” doesn’t mean “trap-free”. A nil channel that blocks forever, a channel closed twice that causes a panic, a timer created anew on every loop iteration instead of being reused… The book covers almost all of them, and having them in one place is very valuable.
It’s one of those links worth keeping at hand and passing on to anyone who starts working seriously with Go. If you already know the language, read it from start to finish once: you’ll surely find a couple of things you thought you knew and didn’t quite.




