Mutex
One wand. One stir at a time. Hand it on as you leave, or the brew is undone.
package main
import (
"fmt"
"sync"
"time"
)
// Cauldron: a shared brew. Stirs must be tallied accurately, or the
// spell will not come out right.
type Cauldron struct {
mu sync.Mutex
stirs int
}
// Stir takes the wand, adds a notch to the count, and gives the wand
// back. Only one apprentice may hold it at a time.
func (c *Cauldron) Stir(stirMs int) {
c.mu.Lock() // take the wand of stirring
defer c.mu.Unlock() // pass it on as we leave
time.Sleep(time.Duration(stirMs) * time.Millisecond)
c.stirs++
}
func main() {
var wg sync.WaitGroup
c := &Cauldron{}
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Stir(1500)
}()
}
wg.Wait()
fmt.Printf("brew complete; %d stirs tallied\n", 4)
} What you are witnessing
A sync.Mutex is a wand of stirring. It exists in one of two states:
held, or laid down on the bench. Lock() picks it up; if another
goroutine already has it, the caller waits — patiently, indefinitely — until the
wand is returned. Unlock() sets it back down, and the next waiter is
woken to take it. Code between the two calls is the critical section: the
one stretch of work that no two apprentices may perform at the same time.
In the workshop above, the apprentices each carry a single reagent and queue up to drop it into the shared cauldron. With the mutex on, the wand is passed solemnly from hand to hand; every reagent goes in cleanly, in order, and the brew completes in a shower of stars. Turn the toggle off and watch the same apprentices lunge at once: ghost-wands proliferate, reagents collide in mid-air, and the cauldron belches purple smoke — a textbook race condition.
Why a wizard cares
Concurrent code that touches shared state without coordination doesn't merely
risk the wrong answer — it risks a different wrong answer on
every run, often only in production, often only under load. Go's race detector
(go test -race, go run -race) can catch many such bugs,
but the surest defence is to declare what you protect and protect it the same way
every time. A Mutex is the smallest, sturdiest declaration there is.
The shape of the spell
- Hold the wand only as long as you must. The critical section is the bit between
LockandUnlock. Anything not touching shared state should sit outside it — long sleeps, network calls, and other apprentices' work do not belong inside. - Pair
Lockwithdefer Unlock.defer mu.Unlock()at the top of the critical section survives panics, early returns, and your own forgetfulness — exactly asdefer wg.Done()does for aWaitGroup. - Pass by pointer. A
Mutexmust not be copied after first use. Embed it in a struct and pass the struct as*Cauldron, never asCauldron. - Reads as well as writes. If one goroutine writes a field and another reads it, both sides need the lock. For workloads that are overwhelmingly read-only, reach for
sync.RWMutex— many readers, one writer.
Pitfalls (the asterisks every wizard must read)
- Deadlock. Two goroutines, each holding one wand and waiting for the other, will wait forever. Always acquire multiple locks in the same order, everywhere.
- Unlocking an unlocked mutex panics. Match every
Lockwith exactly oneUnlock, on every path. - Copying a
Mutexafter use is a silent disaster. The copy's state diverges from the original; one cauldron becomes two, and neither is what you wanted.go vetwill warn you when it can spot this. - A mutex is not a queue. Waiters are woken in an undefined order. If you need fairness or priority, you need more than a
Mutex. - Holding the wand across a channel send or RPC is asking for trouble. The longer the critical section, the more your other apprentices stand idle — and the more likely a slow remote turns into a deadlock involving someone else's lock too.
Reagents required: sync, already in your standard pouch. No newt eyes.