Go

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

All 38 Concurrency Patterns & sync questions