Intermediate · Concurrency: goroutines, channels and context · topic 29 of 46 · 35 min
Mutex, Once, atomic and the race detector
Explanation
A race in five lines
count := 0
var wg sync.WaitGroup
for range 1000 {
wg.Add(1)
go func() {
defer wg.Done()
count++ // read, add, write: three steps, not one
}()
}
wg.Wait()
fmt.Println(count) // often less than 1000, and different each runA data race is when two goroutines access the same memory at the same time, at least one of them writes, and nothing orders the accesses. Races cause wrong totals, corrupted maps, and crashes that happen once a week in production and never on your laptop.
The race detector
go run -race main.go
go test -race ./...
# ==================
# WARNING: DATA RACE
# Read at 0x... by goroutine 8: ...
# Previous write at 0x... by goroutine 7: ...sync.Mutex: one at a time
A mutex (mutual exclusion lock) protects a critical section: code that only one goroutine may run at a time. Lock waits until the mutex is free and takes it; Unlock releases it. The zero value of a Mutex is ready to use.
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}RWMutex: many readers, one writer
type PriceList struct {
mu sync.RWMutex
prices map[string]int
}
func (p *PriceList) Get(item string) (int, bool) {
p.mu.RLock() // many readers at once
defer p.mu.RUnlock()
v, ok := p.prices[item]
return v, ok
}
func (p *PriceList) Set(item string, v int) {
p.mu.Lock() // writers are exclusive
defer p.mu.Unlock()
p.prices[item] = v
}sync.Once: run set-up exactly once
var (
once sync.Once
config map[string]string
)
func Config() map[string]string {
once.Do(func() {
fmt.Println("loading config...")
config = map[string]string{"region": "ap-south-1"}
})
return config
}sync/atomic: fast counters
var hits atomic.Int64 // Go 1.19+ typed atomics
func handler() {
hits.Add(1)
}
func report() {
fmt.Println("hits:", hits.Load())
}Channels or mutex?
| Use channels when | Use a mutex when |
|---|---|
| Passing ownership of data from one goroutine to another | Several goroutines read and update shared state (a cache, a counter, a map) |
| Building pipelines and worker pools | The critical section is small and simple |
| Signalling events (done, cancel) | You would otherwise need a channel just to protect a variable |
Both are correct tools. Pick the one that makes the code simplest to read. sync.Map exists for special cases (keys written once and read many times, or goroutines working on separate keys), but a plain map plus a mutex is the usual choice.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Mutex, Once, atomic and the race detector · 1/7
When goroutines share data, you must protect it. Otherwise you get data races.
Prefer a video by a teacher? Find free videos on “golang mutex rwmutex sync once atomic race detector tutorial” on YouTube ↗
In real life: A rate-limited OTP counter
An OTP service allows at most 3 OTP requests per phone number per window. Requests arrive on many goroutines at once (net/http runs each in its own goroutine), so the map of counts is protected by a mutex. Without it, the map could be corrupted or Go could crash with "concurrent map writes".
package main
import (
"fmt"
"sync"
)
type OTPLimiter struct {
mu sync.Mutex
counts map[string]int
limit int
}
func (l *OTPLimiter) Allow(phone string) bool {
l.mu.Lock()
defer l.mu.Unlock()
if l.counts[phone] >= l.limit {
return false
}
l.counts[phone]++
return true
}
func main() {
l := &OTPLimiter{counts: map[string]int{}, limit: 3}
var wg sync.WaitGroup
var allowed sync.Map // just to show the results safely
for i := range 10 {
wg.Add(1)
go func() {
defer wg.Done()
allowed.Store(i, l.Allow("98XXXXXX10"))
}()
}
wg.Wait()
n := 0
allowed.Range(func(_, v any) bool {
if v.(bool) {
n++
}
return true
})
fmt.Println("allowed:", n) // always 3
}Practice (0/4 done)
1000 goroutines each run count++ on a shared int with no lock. What is printed afterwards?
Which line is the bug? 1: type Stats struct { mu sync.Mutex; n int } 2: func (s Stats) Inc() { 3: s.mu.Lock() 4: defer s.mu.Unlock() 5: s.n++ 6: }
Which tool reports "WARNING: DATA RACE"?
Write a SafeBalance type with a mutex-protected int64 balance and methods Deposit, Withdraw (returns an error if not enough) and Balance. Start 100 goroutines that each deposit 10 and check the final balance is 1000. Run it with -race.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- context: timeouts and cancellationConcurrency: goroutines, channels and context · same module
- Channels: passing data between goroutinesConcurrency: goroutines, channels and context · same module
- Concurrency 1: threads, mutexes and data racesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
- Shared state: Arc, Mutex, Send and SyncRust Programming · Concurrency and async
- Concurrency interview questions and coding roundsCareers and interviews
- Concurrency 2: atomics, async, futures and condition variablesC++ Programming (modern C++ and DSA) · Exceptions, concurrency and building projects with CMake
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is a data race?
Answer: Unsynchronised concurrent access to the same memory where at least one access is a write
That is the precise definition used by the race detector.
When is RWMutex a good choice?
Answer: When reads greatly outnumber writes
Readers can hold the lock together; writers get exclusive access.
What does sync.Once guarantee?
Answer: A function runs at most once, even if called from many goroutines
Once.Do runs its function exactly once.
Is the zero value of sync.Mutex ready to use?
Answer: Yes
var mu sync.Mutex is an unlocked, usable mutex.
Which is best for a simple hit counter shared by many goroutines?
Answer: atomic.Int64
atomic.Int64 gives safe, fast Add and Load.
What is a mutex for?Related
Answer: Allowing only one thread at a time into a section that touches shared data
Mutual exclusion prevents simultaneous access.
What does a Mutex guarantee?Related
Answer: Only one thread accesses the data at a time
Mutual exclusion is its purpose.
Which flag finds data races in concurrent code?Related
A goroutine panics without recover. What happens?Related
Answer: The whole program crashes
An unrecovered panic in any goroutine terminates the process.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Concurrency: goroutines, channels and context” before its exam: context: timeouts and cancellation.
- Module 6 exam: Concurrency · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Standard library essentials. 46 topics left in the course before the final exam.