2199
package main
import "fmt"
func main() {
var done bool
var msg string
go func() {
msg = "hello"
done = true
}()
for !done {
}
fmt.Println(msg)
}
Can you imagine - sometimes will be printed an empty string.
-race reports data race here.
#gemini 3.8 flash high explanation:
Goroutines do not have separate memory blocks; all goroutines share the same virtual address space.
An empty string can still be printed, but that happens because of compiler/hardware reordering and cache visibility, caused by a data race.
### Why this happens
1. Shared Address Space:
Both variables (done and msg) reside in shared memory. Goroutines are lightweight threads managed by the Go runtime, not isolated processes.
2. No "Happens-Before" Guarantee:
According to the Go memory model, reads and writes across goroutines are only ordered when synchronized through mechanisms like channels, mutexes, or atomic operations. Without synchronization, the Go compiler and the CPU are free to:
• Reorder the writes msg = "hello" and done = true.
• Reorder the reads in main.
• Keep msg or done in CPU registers or store buffers without flushing to shared cache.
If done = true becomes visible to main before msg = "hello" does, fmt.Println(msg) prints "".
3. Infinite Loop Risk:
The code might also never terminate. The compiler can observe that done is not modified inside the body of for !done {} in main and hoist the read or keep done in a register:
// What the compiler is allowed to generate:
if !done {
for {
}
}
### Detection
You can detect this issue using the Go race detector:
go run -race main.go
### Proper Solution
Use synchronization tools such as channels, sync.WaitGroup, or atomic values to establish a happens-before relationship:
package main
import (
"fmt"
"sync"
)
// main coordinates goroutines using sync.WaitGroup to guarantee visibility.
func main() {
var (
wg sync.WaitGroup
msg string
)
wg.Add(1)
go func() {
defer wg.Done()
msg = "hello"
}()
wg.Wait()
fmt.Println(msg)
}