Go Concurrency — Goroutines, Channels & the CSP Model
A different model: communicate, don't share
Everything so far — locks, atomics, the memory model — is the shared-memory model: many threads touch the same variable, and you guard it. Go takes the opposite stance, CSP (Communicating Sequential Processes):
"Do not communicate by sharing memory; instead, share memory by communicating." — Go proverb
Instead of locking a shared value, you give ownership of data to one goroutine and pass it to others over a channel. The race can't happen because only one goroutine touches the data at a time. Same problems (visibility, races, deadlock) — different default tool.
Goroutines: threads, but cheap
A goroutine is a function scheduled by the Go runtime onto a small pool of OS threads. They
start at ~2 KB of stack and grow on demand, so a program can run hundreds of thousands — where
OS threads top out in the thousands. You launch one with go, and wait with a WaitGroup:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) { defer wg.Done(); work(id) }(i) // runs concurrently
}
wg.Wait() // block until all three Done()
The same race — and Go's killer feature, the race detector
CSP is a discipline, not a guarantee: share a variable across goroutines and you get the exact race from the first lesson.
counter := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); counter++ }() // DATA RACE
}
wg.Wait()
fmt.Println(counter) // < 1000, varies
What Go gives you that the JVM doesn't out of the box: go run -race instruments memory access and
prints the exact two stacks that raced. Run it in CI and races stop being heisenbugs. The three fixes:
// 1. Mutex (shared-memory style)
var mu sync.Mutex
mu.Lock(); counter++; mu.Unlock()
// 2. Atomic
var counter int64
atomic.AddInt64(&counter, 1)
// 3. CSP style: one goroutine owns the counter; others send deltas on a channel
inc := make(chan int)
go func() { for d := range inc { counter += d } }() // sole owner — no lock needed
inc <- 1
Channels: typed pipes between goroutines
A channel carries values of one type between goroutines. Unbuffered (make(chan T))
is a synchronous handoff — the send blocks until someone receives, so it doubles as synchronization.
Buffered (make(chan T, n)) is a bounded queue — the send blocks only when full
(natural back-pressure).
ch := make(chan int) // unbuffered
go func() { ch <- 42 }() // send (blocks until received)
v := <-ch // receive -> 42
The worker pool — Go's answer to a thread pool
No ExecutorService: you just start N goroutines that range over a shared jobs channel
and push to a results channel. The channel is the queue and the synchronization.
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 0; w < 3; w++ { // 3 workers
go func() { for j := range jobs { results <- process(j) } }()
}
for _, t := range tasks { jobs <- t }
close(jobs) // sender closes -> workers' range ends
for range tasks { total += <-results } // collect
select — wait on many channels, with timeout and cancellation
select {
case v := <-ch: use(v)
case <-time.After(time.Second): timeout() // no message in 1s
case <-ctx.Done(): return ctx.Err() // cancellation/deadline
}
select is the CSP analogue of condition variables: instead of wait/notify on a monitor,
you block on whichever channel becomes ready first.
Java ↔ Go, side by side
| Concept | Java (shared memory) | Go (CSP) |
|---|---|---|
| Unit of concurrency | Thread / Runnable | goroutine (go f()) |
| Wait for completion | join() / Future | sync.WaitGroup / channel |
| Mutual exclusion | synchronized / ReentrantLock | sync.Mutex |
| Atomic counter | AtomicInteger | sync/atomic |
| Signaling / waiting | wait() / notify() | channels + select |
| Thread pool | ExecutorService | goroutines over a jobs channel |
| Visibility | volatile / happens-before | channel send/recv establishes happens-before |
| Default mindset | share state, guard with locks | pass ownership, communicate |
Pitfalls
- Deadlock: if every goroutine is blocked, the runtime panics
fatal error: all goroutines are asleep - deadlock!— Go catches the total-deadlock case for you (the JVM just hangs). - Goroutine leak: a goroutine blocked forever on a channel nobody sends to (or never closed)
leaks silently — no error, just growing memory. Always have an exit path (close, or
ctx.Done()). - Closing channels: only the sender closes; sending on a closed channel panics;
closing twice panics. Receivers detect close via
v, ok := <-ch. - CSP isn't magic: you can still share memory and race — keep
-racein CI.
Takeaways
- Go = CSP: pass data over channels instead of guarding shared state with locks.
- Goroutines are cheap;
WaitGroupwaits, channels synchronize,selectmultiplexes. - The same races exist — but
go run -racefinds them. Bothsync.Mutex/atomics (shared-memory) and channels (CSP) are valid; pick per problem. - Unbuffered channel = handoff; buffered = bounded queue. Sender closes, never the receiver.
Re-authored for this guide; worker-pool and channel diagrams hand-authored as SVG. Follows Go's "Share Memory By Communicating" blog post, Rob Pike's CSP talks, and the Go memory model. Cross-links: Race Conditions, Memory Model & Visibility, Atomics & CAS.
🤖 Don't fully get this? Learn it with Claude
Stuck on Go Concurrency — Goroutines, Channels & the CSP Model? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Go Concurrency — Goroutines, Channels & the CSP Model** (Concurrency) and want to truly understand it. Explain Go Concurrency — Goroutines, Channels & the CSP Model from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Go Concurrency — Goroutines, Channels & the CSP Model** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Go Concurrency — Goroutines, Channels & the CSP Model** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Go Concurrency — Goroutines, Channels & the CSP Model** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.