Channels or mutexes: how do you decide? ("Share memory by communicating")
Question 253MediumGo 1.22 to 1.25
The proverb is guidance, not a rule. Choose by the shape of the problem.
Use channels when you are:
- transferring ownership of data (a producer hands work to a consumer),
- coordinating goroutine lifecycles such as done signals, pipelines and fan-in/fan-out,
- distributing work,
- signalling events or timeouts that need to combine with
select.
Use mutexes or atomics when you are:
- protecting internal state of a struct (caches, counters, maps),
- running short critical sections,
- in a spot where performance matters: an uncontended mutex costs about 10-20ns, a channel operation noticeably more.
// Mutex: guarding state
type Stats struct {
mu sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(k string) {
s.mu.Lock()
s.hits[k]++
s.mu.Unlock()
}
// Channel: ownership handoff / coordination
results := make(chan Result)
go func() { results <- compute() }()
select {
case r := <-results:
use(r)
case <-ctx.Done():
return ctx.Err()
}
Anti-patterns:
- A channel of capacity 1 used as a mutex.
- A "manager goroutine" that serializes every map access through channels, adding latency and a goroutine to shut down.
- Exposing a mutex in a public API. Keep it unexported and hide the synchronization behind methods.
Senior answer: encapsulate the concurrency so callers do not need to know which one you picked.
More on Concurrency Patterns & sync
- Q251What happens here, and when does the runtime NOT detect a deadlock?
- Q252Design a thread-safe loading cache. Why is naive double-checked locking with a plain flag wrong in Go?
- Q254How do you test concurrent and time-dependent code deterministically? (testing/synctest)
- Q255What does this print? Can recover in main catch a panic from another goroutine?
- Q256What is false sharing, how does it hurt concurrent Go code, and how do you fix it?
- Q257How is GOMAXPROCS chosen in containers, and what changed in Go 1.25?