13 · Concurrency Deep Dive (Interview Level)¶
You know go func(). Interviews test whether you know why it's fast and where it breaks.
The GMP Scheduler¶
Editable draw.io source — open in draw.io Desktop to tweak.
What interviews actually ask¶
| Question | The answer they're fishing for |
|---|---|
| Goroutine vs Java thread? | ~KB stacks vs ~1MB; runtime-scheduled (M:N), not OS-scheduled; no heavyweight context switch |
What is GOMAXPROCS? |
Number of Ps — max OS threads executing Go code simultaneously. Default = CPU count |
| How does a blocking syscall work? | M detaches from P and blocks in the kernel; another M picks up the P and keeps running Gs |
| Network I/O blocking? | Doesn't block an M — goes through the netpoller (epoll/kqueue), G is parked and rescheduled |
| Preemption? | Since Go 1.14: async preemption — a tight for {} can no longer starve the scheduler |
Channels — beyond basics¶
ch := make(chan int) // unbuffered: send blocks until receive (rendezvous)
ch := make(chan int, 3) // buffered: blocks only when full
nilchannel: send and receive block forever — used to disable aselectcase by setting it to nil.- Receive from closed channel: zero value,
ok == false. Send to closed channel: panic. for v := range chexits when channel is closed — idiom for "work queue drained".- Who closes? The sender, never the receiver (receiver can't know if more senders exist).
select¶
select {
case v := <-ch1: // receive
case ch2 <- v: // send
case <-ctx.Done(): // cancellation (the idiomatic timeout)
case <-time.After(t): // timeout — leaks the timer in a loop! use time.NewTimer
default: // non-blocking
}
// Multiple ready cases → chosen uniformly at RANDOM (not first-ready)
context.Context — the interview favorite¶
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel() // ALWAYS call — leaks a timer + goroutine otherwise
- Immutable tree:
WithCancel,WithTimeout,WithDeadline,WithValue. - Pass as first param
ctx context.Context; never store in a struct (controversial but standard advice). ctx.Err()→context.Canceledorcontext.DeadlineExceeded.WithValueis for request-scoped data (trace IDs), not optional function params — interviewers probe this.
Synchronization — when channels aren't right¶
| Tool | Use when |
|---|---|
sync.Mutex / RWMutex |
Protecting shared state; RWMutex when reads ≫ writes |
sync.WaitGroup |
Wait for N goroutines to finish |
sync.Once |
Lazy one-time init (singleton) |
sync.Map |
Rare: write-once-read-many caches; usually plain map+RWMutex wins |
atomic.* |
Single counters/flags; lock-free |
errgroup |
Fan-out/fan-in with first-error cancellation + Wait() |
Classic interview questions¶
Why does this print the same value?
Loop variable capture. Pre-1.22 fix: passi as a param. Go 1.22+ fixed it — each iteration gets a fresh i. Know both.
Unbuffered channel send with no receiver — what happens?
Sender blocks forever → goroutine leak. The goroutine never dies, GC can't collect it. Fix: buffered channel, select+ctx.Done(), or always ensure a receiver.
Detect a data race?
go run -race / go test -race. Detector instruments memory access; ~5-10x slowdown, run in CI not prod.
time.After in a hot loop?
Leaks a timer per iteration until it fires. Use time.NewTimer + Stop(), or restructure with select.
How to limit concurrency on 10k tasks?
Semaphore pattern: buffered channel sem := make(chan struct{}, 64); sem <- struct{}{} before work, <-sem after. Or errgroup + SetLimit(64).
Check Your Understanding¶
A goroutine making a blocking network read…
Sending on a closed channel causes:
select with two ready cases picks: