What does this counter print, and how do you prove the bug?
func main() {
var n int
var wg sync.WaitGroup
for range 1000 {
wg.Add(1)
go func() {
defer wg.Done()
n++ // data race
}()
}
wg.Wait()
fmt.Println(n) // usually < 1000, varies by run
}
The output is undefined. On multi-core machines it is usually less than 1000 and changes from run to run. n++ is a read, an add, and a write. Two goroutines can read the same value, and one increment is lost. Formally, this is a data race, and the memory model gives no guarantees at all for racy programs. For a single aligned machine word the program will not crash in practice, but it reads stale values and loses updates. For multi-word values (strings, slices, interfaces), a race can produce torn values and real memory corruption.
Prove it with the race detector: go run -race main.go reports WARNING: DATA RACE with both stack traces. Run tests with -race in CI.
Fixes, from cheapest to most flexible:
var n atomic.Int64
n.Add(1) // lock-free
var mu sync.Mutex
mu.Lock()
n++
mu.Unlock() // for multi-field invariants
Or have each goroutine send its result on a channel and add them up in one goroutine. The interviewer wants you to avoid saying "it's fine because it's just an int" and to know that GOMAXPROCS=1 hides the race without fixing it.
More on Tricky Output & Code-Review Puzzles
- Q509Why does errors.Is return different results here?
- Q510Go 1.23 range-over-func: what does this buggy iterator do?
- Q512Deleting from a slice in place: what do both lines print?
- Q513Embedding is not inheritance: what does this print, and which line does not compile?
- Q514Type switch puzzle: what does describe return for each input?
- Q515Package initialization order: what does this program print?