Dev Skills

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.

Topic 89 of 98 · 5 min to read · 20 min to build
After this topic you can
  • 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.MutexChannel
What it doesLets one goroutine at a time use shared dataHands a value from one goroutine to another
Best forCaches, counters, state inside a structPassing ownership of data, handing out work, sending results
Think of it asA lock on a doorA pipe between workers
Typical codemu.Lock(); defer mu.Unlock()ch <- v and v := <-ch
Common mistakeForgetting to unlock, copying a mutexUsing channels for simple state; leaking blocked goroutines
SpeedFaster for a short critical sectionSome 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.

counter.go
package main
import (
"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.

Watch out: Whichever you choose, run your tests with go test -race. The race detector finds the bugs both tools are meant to prevent.
The short version
  • 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.
Remember: State → mutex. Flow of data or work → channel. If both fit, pick the simpler one.
Then try these in your own editor
  1. 1You keep a map of logged-in users that many HTTP handlers read and a few update. Mutex or channel?
  2. 2You have 1,000 image files to resize using 8 workers. Mutex or channel?
  3. 3Remove the mutex from counter.go and run with go run -race. What happens?