Mutex vs channel in Go: when to use which
Both make concurrent code safe. The Go team’s own advice is to use whichever is simpler.
- Choose a mutex or a channel for a given problem and say why in one line.
- Write the same safe counter both ways.
- Quote the Go wiki guidance on mutexes versus channels.
A mutex protects shared data so only one goroutine touches it at a time. A channel moves data from one goroutine to another. The Go wiki page "Use a sync.Mutex or a channel?" says: use whichever is most expressive and/or most simple.
| sync.Mutex | Channel | |
|---|---|---|
| What it does | Lets one goroutine at a time use shared data | Hands a value from one goroutine to another |
| Best for | Caches, counters, state inside a struct | Passing ownership of data, handing out work, sending results |
| Think of it as | A lock on a door | A pipe between workers |
| Typical code | mu.Lock(); defer mu.Unlock() | ch <- v and v := <-ch |
| Common mistake | Forgetting to unlock, copying a mutex | Using channels for simple state; leaking blocked goroutines |
| Speed | Faster for a short critical section | Some overhead per send; fine for most work |
Should I use a mutex or a channel in Go?
Use a channel when you are passing data or work between goroutines. Use a mutex when several goroutines share some state, like a map or a counter. If both work, pick the one with less code. That is the Go wiki’s advice.
The wiki adds: "A common Go newbie mistake is to over-use channels and goroutines just because it’s possible, and/or because it’s fun." A mutex is not un-Go-like.
What does "share memory by communicating" mean?
It means: instead of many goroutines locking the same variable, let one goroutine own the data and have others send it messages. It is a Go proverb, a design idea, not a rule that bans mutexes.
How do you write a safe counter both ways?
With a mutex, each goroutine locks, adds, unlocks. With a channel, one goroutine owns the count and the others send it "+1" messages. Both print 1000. The mutex version is shorter, so for a counter it wins.
package mainimport ( "fmt" "sync")// Mutex: shared state, locked around each change.type Counter struct { mu sync.Mutex n int}func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.n++}func main() { var wg sync.WaitGroup var c Counter for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait() fmt.Println("mutex counter:", c.n) // Channel: one goroutine owns the count; others send it messages. incs := make(chan struct{}) done := make(chan int) go func() { n := 0 for range incs { n++ } done <- n }() for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() incs <- struct{}{} }() } wg.Wait() close(incs) fmt.Println("channel counter:", <-done)}When is a channel clearly better?
When the problem is about flow: a worker pool handing out jobs, a pipeline of stages, waiting for the first of several results, or a timeout with select. Doing those with a mutex means writing your own queue and signalling. See Channels and select and Concurrency patterns.
When is a mutex clearly better?
When goroutines read and update the same state in place: a cache, a map of sessions, a struct’s fields. Use sync.RWMutex if reads far outnumber writes. See Mutexes and races.
go test -race. The race detector finds the bugs both tools are meant to prevent.- Mutex: guard shared state. Channel: move data or work between goroutines.
- Go wiki: use whichever is most expressive and/or most simple.
- Over-using channels is a known beginner mistake.
- "Share memory by communicating" is a design idea, not a ban on mutexes.
- Always test concurrent code with
-race.
- 1You keep a map of logged-in users that many HTTP handlers read and a few update. Mutex or channel?
- 2You have 1,000 image files to resize using 8 workers. Mutex or channel?
- 3Remove the mutex from
counter.goand run withgo run -race. What happens?