Why does this count++ across 1000 goroutines not reliably reach 1000, and how do you fix it?
This program increments a shared count from 1000 goroutines and prints the total. It does not reliably print 1000.
var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); count++ }()
}
wg.Wait()
fmt.Println(count)
Find and fix the bug.
count++ is a read-modify-write, not atomic. Many goroutines read the same value, increment, and write back, so increments are lost and the total is nondeterministic — go run -race flags the data race. Fix with a mutex around count++, or atomic.AddInt64(&count, 1) with count as int64.
- ✗Believing
count++is a single atomic instruction rather than read-modify-write - ✗Blaming the
WaitGroupinstead of the unsynchronized shared write - ✗Thinking a
time.Sleepor cache flush fixes a data race
- →Why is
atomic.AddInt64cheaper than a mutex for a single counter? - →What does the
-racedetector actually observe to flag this?
Find the bug
var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); count++ }()
}
wg.Wait()
fmt.Println(count) // not reliably 1000
Why it is not 1000
count++ looks like one operation, but it is really three: read count, add 1, write it back. When a thousand goroutines do this concurrently with no synchronization, two of them can read the same value, add 1, and write — one increment is lost. The total comes out random and usually below 1000.
This is a classic data race; go run -race points at the concurrent access to count.
✅ Fixes:
// 1) mutex
var mu sync.Mutex
go func() { defer wg.Done(); mu.Lock(); count++; mu.Unlock() }()
// 2) atomic counter (cheaper for a single variable)
var count int64
go func() { defer wg.Done(); atomic.AddInt64(&count, 1) }()
⚠️ A time.Sleep does not fix a race — it only masks it on one particular run.