Go Memory Model and Safe Publication
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Read Go pointers, struct initialization and atomic pointer method signatures.
What you will practice
- Trace the exact synchronization observation that publishes immutable state.
- Disprove a Load-then-Store Swap using a concurrent predecessor history.
In this lesson
Lesson
4 min readGo Memory Model and Safe Publication
Implement a cell whose readers observe a complete immutable integer snapshot while writers may Store or Swap concurrently. Prepare by tracing pointers, struct initialization and goroutine interleavings. The key skill is naming an ordering edge; "it usually runs first" is not one.
Establish a happens-before path
The Go memory model relates ordinary program order to synchronization. For publication, trace this chain:
writer initializes snapshot.value = 42
-> atomic Store publishes its pointer
-> atomic Load observes that pointer
-> reader accesses snapshot.value
The observing Load orders prior initialization before the read. The writer must finish initialization before publication. Sleeping, a goroutine finishing, or an ordinary boolean flag does not supply the required observation. Atomic publication also does not synchronize later ordinary writes to the snapshot. Keep a published snapshot immutable and allocate a new one for an update.
The integer field is ordinary memory; the pointer operation supplies the ordering. Making a pointer atomic does not make an entire reachable map or slice safe to mutate concurrently. A shallow struct copy with a shared mutable map would need an additional ownership or synchronization design.
Work the cell state and its zero value
PublishedCell distinguishes an absent pointer from a present snapshot containing
zero. That distinction carries through every method:
| Operation | Snapshot afterward | Returned value |
|---|---|---|
| Load on empty cell | absent | (0,false) |
| Store(0) | present value 0 | none |
| Load | present value 0 | (0,true) |
| Swap(7) | present value 7 | (0,true) |
| Swap(-2) | present value -2 | (7,true) |
A zero-value cell behaves as empty. For a nil receiver, Store does nothing,
and Load/Swap return (0,false). Do not copy a cell after its atomic field
has been used. These are separate rules: absent content is not the same as
an invalid pointer receiver, even though the empty read results agree.
Swap needs one indivisible replacement
A Load followed by Store can be race-free yet violate Swap's semantics. Start with value 10 and two goroutines swapping 20 and 30:
| Step | Goroutine A | Goroutine B | Cell |
|---|---|---|---|
| 1 | Load returns 10 | 10 | |
| 2 | Load returns 10 | 10 | |
| 3 | Store 20 | 20 | |
| 4 | Store 30 | 30 |
Both report predecessor 10. No serial ordering of two swaps permits this: if A comes first, B must return 20; if B comes first, A must return 30. An atomic pointer Swap returns the replaced pointer at the same instant it publishes the new pointer. Load+Store provides visibility but not this compound operation.
A useful test starts with 0 and concurrently swaps each unique value 1..N. The returned predecessors plus the final value must contain every value 0..N exactly once. The test does not assume a scheduler order. Run it with the race detector too; race freedom alone cannot prove the predecessor property.
A channel edge depends on what was observed
For x=42; close(done), a receive that observes done's closure can then read x,
provided no other writes race. With a buffered channel, receiving an older queued
token does not necessarily observe the later close. If the write happened after
that token was sent, this receive has no publication edge for that write.
Similarly, unlocking a mutex orders a later successful lock of that same mutex; unlocking an unrelated mutex does not help. State the exact shared synchronization object and matched operations when explaining a proposed implementation.
Predict before coding
A cell contains 4. Two true atomic swaps install 8 and 12. The final value is 8. What must the swap-to-8 return, and what must the swap-to-12 return?
Check the only compatible order
Swap-to-12 happened first and returned 4; swap-to-8 happened second and returned 12. The final value determines the last swap, even though the goroutine start order tells us nothing. This is an operation-order claim, not a timing guess.
Implement the exercise, run its tests and the race detector in your solution workspace, then independently design immutable config publication containing a map. Explain how callers are prevented from mutating that map after publication. If updates must merge concurrent changes rather than replace the snapshot, identify why plain Store/Swap needs a stronger update protocol such as a CAS retry loop or a lock.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.