Lesson 16 · Concurrency & context

sync primitives

When you do share memory — Mutex, WaitGroup, Once.

Your win: use Mutex, WaitGroup, and Once correctly, and answer the classic question — "channels or a mutex?"

The three you'll actually use

sync.Mutex

Guards shared state. Lock() before touching it, Unlock() after (usually defer mu.Unlock()). RWMutex allows many concurrent readers.

sync.WaitGroup

Wait for a set of goroutines. Add(n) before launching, Done() as each finishes, Wait() to block until all are done.

sync.Once

Do(f) runs f exactly once, ever — for lazy init or an idempotent "close this channel only once".

Remember Lesson 2: their zero values are readyvar mu sync.Mutex works with no init.

Your Kafka engine uses all three

internal/golibs/kafka/kafka.go · conversationmgmt/.../taskrunner/task_runner.go
// WaitGroup: await every consumer goroutine before shutting down
k.consumerWaitGroup.Add(1)  ...  k.consumerWaitGroup.Wait()   // kafka.go:587,825

// Mutex: guard the shared "which consumers are running" state
k.mutex.Lock(); member.setRunning(false); k.mutex.Unlock()    // kafka.go:590

// Once: close a channel exactly once, even if reached from many paths
closeOnce.Do(func() { close(ch) })                            // task_runner.go
Why Once for channel close Closing an already-closed channel panics (Lesson 15). When several code paths might all try to close it, sync.Once makes the close idempotent — a small, idiomatic guard our task_runner and worker pool both use.

Channels or a mutex? (the interview question)

The decision Use a channel to transfer ownership of data or hand work between goroutines (share memory by communicating). Use a mutex to protect shared state that stays put and is read/written in place — a counter, a cache, a status map. The Go team's advice: use whichever makes the code simpler.1

Our engine shows both in one place: messages flow through channels, but the "is this consumer running?" map is shared state, so it's guarded by a mutex. Forcing everything through channels there would be more code, not less.

Mutex gotchas A Mutex is not reentrant — locking it twice in the same goroutine deadlocks. And never copy a struct containing a Mutex (copies the lock state); that's why such structs are passed by pointer (Lesson 3). go vet catches the copy mistake.
Read this next

Effective Go — Concurrency + the sync package

Effective Go frames the channel-vs-mutex choice; the sync docs give the exact semantics of each primitive.

go.dev/doc/effective_go#concurrency
pkg.go.dev/sync

Check yourself (from memory)

Q1. sync.WaitGroup is for…

Add/Done/Wait — block until a set of goroutines completes. Our shutdown waits on the consumer group.

Q2. sync.Once's Do(f) guarantees f

Exactly once across all callers — ideal for lazy init or an idempotent channel close.

Q3. Prefer a mutex over a channel when you…

Shared state that stays put (a map, counter) → mutex. Handing data between goroutines → channel. Simpler wins.
Channel or mutex — how do you choose?
recall, then click to reveal
Use a channel to transfer ownership of data or coordinate/hand off between goroutines (share memory by communicating). Use a mutex to protect shared state that stays put and is read/written in place (counter, cache, status map). Rule of thumb: whichever is simpler. Our Kafka engine — channels carry messages, a mutex guards the running-state map, a WaitGroup awaits shutdown.
Want to see RWMutex, sync.Map, or atomic and when each earns its place over a plain Mutex? Ask me.

1. Effective Go — concurrency; sync package docs; Go Wiki — Mutex or Channel.