Go Memory Model
The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.
Memory Model Basics
Happens Before
The key concept in Go's memory model is "happens before", which defines when the effects of one goroutine are guaranteed to be visible to another goroutine. This relationship is the fundamental building block of synchronization in Go.
var a, b int
func f() {
a = 1 // A
b = 2 // B
}
func g() {
print(b) // C
print(a) // D
}
Without synchronization, there's no guarantee about the order in which these operations will be observed by different goroutines.
Initialization Guarantee
Go provides important guarantees about package initialization:
var a = 1 // Package level variable
func init() {
a = 2 // Guaranteed to happen before main starts
}
func main() {
print(a) // Always prints 2
}
Synchronization Mechanisms
Channel Communication
Channel operations in Go provide strong synchronization guarantees:
var done = make(chan bool)
var msg string
func setup() {
msg = "hello"
done <- true // Send guarantees msg is visible to receiver
}
func main() {
go setup()
<-done // Receive guarantees msg is visible here
print(msg) // Guaranteed to print "hello"
}
Mutex Synchronization
Mutex provides mutual exclusion and memory synchronization:
var mu sync.Mutex
var value int
func modify() {
mu.Lock()
value++ // Protected access
mu.Unlock()
}
func read() int {
mu.Lock()
v := value // Protected access
mu.Unlock()
return v
}
Atomic Operations
Atomic operations provide low-level synchronization:
var flag atomic.Bool
func toggle() {
flag.Store(true) // Atomic store
}
func check() bool {
return flag.Load() // Atomic load
}
Common Pitfalls
Data Races
A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write:
var counter int
func race() {
go func() {
counter++ // Race condition!
}()
counter++ // Race condition!
}
Fix using synchronization:
var counter int
var mu sync.Mutex
func noRace() {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
mu.Lock()
counter++
mu.Unlock()
}
False Sharing
False sharing occurs when variables used by different processors lie on the same cache line:
type Counters struct {
a uint64
b uint64
c uint64
}
Fix by adding padding or using atomic types:
type Counters struct {
a uint64
_ [7]uint64 // padding
b uint64
_ [7]uint64
c uint64
}
Best Practices
- Always use explicit synchronization when sharing data between goroutines
- Prefer channels for communicating between goroutines
- Use the race detector during testing and development
- Document synchronization requirements in your code
- Keep critical sections small when using mutexes
Tools and Debugging
Race Detector
Use the race detector to find synchronization issues:
go test -race ./...
go run -race program.go
Memory Sanitizer
For more thorough memory analysis:
go test -msan ./...
Further Reading
- Official Go Memory Model Documentation
- Synchronization Primitives
- Channel Communication
- Race Conditions
Common Questions
Q: Why isn't my concurrent program behaving deterministically? A: Without proper synchronization, concurrent programs can exhibit non-deterministic behavior due to race conditions.
Q: When should I use channels vs. mutexes? A: Use channels for communication between goroutines and mutexes for protecting shared state.
Q: How can I ensure all goroutines see my updates? A: Use appropriate synchronization mechanisms like channels, mutexes, or atomic operations.