Day 069 — The Go Memory Model¶
Month 3 · Week 2 · ⬅ Day 068 · Day 070 ➡ · Journal index
🎯 Learning Objective¶
Understand the Go memory model: what "happens-before" means, which operations establish it, and why a program free of data races sees writes in a sane order.
📚 Topics¶
- Happens-before and visibility of writes across goroutines
- The synchronisation edges: channels, mutexes,
Once, atomics,go/exit - "Don't be clever": race-free programs behave as if sequentially consistent
📖 Reading / Sources¶
- The Go Memory Model (the canonical spec)
- Russ Cox — Memory Models series (background on happens-before)
- Learning Go (Bodner) ch.10 — "when goroutines see each other's writes"
📝 Notes¶
- The [[memory-model]] defines when a read in one goroutine is guaranteed to observe a write in another. Without a synchronisation edge, there is no guarantee — the compiler and CPU may reorder, cache, or hoist memory operations.
- [[happens-before]] is a partial order on memory operations. If event A happens-before event B, then A's effects are visible to B. Within a single goroutine, program order gives happens-before; across goroutines you must create an edge explicitly.
- A data race is undefined behaviour. A program with a data race has no guaranteed semantics — not "an old value", but anything. The fix is always to add a happens-before edge, never to "hope".
- Edges that establish happens-before:
- Channel: a send happens-before the corresponding receive completes; a
closehappens-before a receive that observes the close; on an unbuffered channel, a receive happens-before the send completes. → [[channel-axioms]] - Mutex: the n-th
Unlockhappens-before the (n+1)-thLockreturns. → [[mutex]] - Once: the single
f()inonce.Do(f)happens-before anyDocall returns. → [[once]] - Atomics: Go's atomics are sequentially consistent; an atomic write happens-before an atomic read that observes it. → [[atomic-operation]]
- Goroutine lifecycle: the
gostatement happens-before the goroutine starts; a goroutine's completion does not by itself synchronise — you need aWaitGroup/channel to observe it. - The classic broken idiom: one goroutine sets
datathen a plaindone = true, another spins ondonethen readsdata. No edge ⇒ the reader may seedone==truebut stale/zerodata, or never seedoneat all. Use a channel, mutex, or atomics. - Practical takeaway: if your program is data-race-free, you can reason about it as if memory were sequentially consistent. All the cleverness lives in getting the synchronisation edges right; the model rewards boring, well-synchronised code.
💻 Code Examples¶
// BROKEN: no happens-before edge between the writer and reader.
var data int
var done bool
func broken() {
go func() { data = 42; done = true }() // plain writes, can be reordered
for !done { // may spin forever or read data==0
}
fmt.Println(data) // not guaranteed to be 42
}
// FIXED: the channel receive happens-after the send, which happens-after data=42.
func fixed() {
data := 0
ch := make(chan struct{})
go func() { data = 42; close(ch) }()
<-ch // establishes happens-before: send/close → receive
fmt.Println(data) // guaranteed 42
}
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Spot the missing happens-before edge (re-read week-2 solutions) | ✅ | exercises/month-03/week-2 |
🐛 Mistakes Made¶
- Believed a plain
boolflag was "good enough" to publish data between goroutines. It isn't — without an edge the reader can see the flag but not the data. Replaced with a channel. - Assumed a goroutine finishing implies its writes are visible to
main. Only true ifmainobserves completion via aWaitGroup/channel.
❓ Open Questions¶
- How do
atomic.Pointer/atomic.Valuepublish a whole struct safely? (The atomic store of the pointer is the edge; readersLoadthe pointer and the pointee's prior writes are visible.)
🧠 Active Recall (answer without looking)¶
- Q: Is "the reader sees an old value" a fair description of a data race?
A
No. A data race is undefined behaviour — the program has no guaranteed semantics at all, not merely a stale read. Add a happens-before edge to fix it.
2. Q: Name three operations that establish a happens-before edge between goroutines. A
A channel send→receive (and close→receive), Unlock→subsequent Lock, once.Do's f → return of Do, and atomic write→atomic read. The go statement also happens-before the new goroutine starts.
🪶 Feynman Reflection¶
The memory model is the rulebook for when one goroutine is allowed to see another's writes. Each synchronisation tool (channel, mutex, Once, atomic) draws an arrow "this happened before that"; without an arrow, the compiler and CPU are free to reorder and you get nonsense. Race-free code has enough arrows that it behaves exactly as written.
🕳️ Knowledge Gaps¶
- The 2022 model's formal acquire/release wording for atomics — I have the intuition (sequential consistency) but not the full formalism.
✅ Summary¶
I understand happens-before, can list the edges Go provides (channels, mutexes, Once, atomics, go), know a data race is UB, and know that race-free programs may be reasoned about as sequentially consistent.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 070: Week 2 review — closed-book recall + re-run everything under
-race.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦🟦⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): the Go memory model and happens-before (day 069)