Day 12 — Memory Model, Integration & Stage III Gate
Day 12 — Memory Model, Integration & Stage III Gate
Day 28 — Memory model lite
Stage III · ~3h (theory-heavy)
Goal: Build a working happens-before intuition for Go’s memory model—enough to reason about races, atomics, channel sync, and mutexes without becoming a formal methods researcher.
You do not need to recite the full Go memory model document. You need to know when a read is guaranteed to observe a write.
Why this day exists
Without a model:
- “It worked on my laptop” becomes architecture
time.Sleepbecomes synchronization
- Atomics get sprinkled randomly
- Channel closes feel magical
With a model:
- You can document why a program is race-free
- You can design simpler ownership
- Race detector results make sense
Theory 1 — Sequential consistency within a goroutine
Inside one goroutine, reads and writes behave as if executed in program order (compiler/CPU reorderings are constrained so you cannot observe them in a single G without concurrency).
The hard part is cross-goroutine visibility.
Theory 2 — Happens-before (HB)
If event A happens before event B, then B is guaranteed to observe the effects of A (for the relevant memory).
Go establishes HB through synchronization events, including:
| Synchronization | HB edge (simplified) |
|---|---|
| Mutex Unlock → Lock | Unlock HB subsequent Lock |
| Channel send → receive | Send completes HB receive completes |
| Channel close → receive zero | Close HB receive that sees closed |
sync.Once Do |
Do’s f() HB return of any Do |
sync.WaitGroup Done → Wait |
Done HB Wait return |
| Atomic ops | Provide ordering depending on type (see docs) |
| Goroutine start | go f initiation HB start of f |
If two accesses to the same location race (no HB either way) and one is a write → data race.
Theory 3 — Initialization guarantees
- Package-level variable initialization completes before
initfunctions
- All
initcomplete beforemain
- A package’s imported dependencies initialize first
You can rely on:
var global = compute() // finished before mainYou cannot rely on unsynchronized writes from other goroutines without HB.
Theory 4 — Incorrect “synchronization”
These do not establish a portable HB for your data:
// NOT synchronization for shared data
time.Sleep(time.Millisecond)
runtime.Gosched()
fmt.Println("debug") // may seem to "fix" races via timingThey may change interleaving probability; they do not make a race correct.
Theory 5 — Channel as memory barrier (practical)
// G1
data = 42
ch <- struct{}{}
// G2
<-ch
fmt.Println(data) // guaranteed to see 42Send/receive pair synchronizes. Similarly, closing and ranging establishes ordering for published data before close.
Mutex pattern
// writer
mu.Lock()
data = 42
mu.Unlock()
// reader
mu.Lock()
v := data
mu.Unlock()Unlock HB next Lock ⇒ reader sees writer’s updates.
Theory 6 — Atomics and flags
var ready atomic.Bool
var data int
// G1
data = 42
ready.Store(true) // publish
// G2
if ready.Load() {
fmt.Println(data) // sees 42 under atomic's ordering for this pattern
}Use atomics deliberately; for multi-field structs prefer:
type snap struct{ /* immutable fields */ }
var p atomic.Pointer[snap]
p.Store(&snap{...}) // publish immutable snapshotReaders Load pointer and use fields without further locks if truly immutable.
Theory 7 — Documenting synchronization
In code reviews, leave a breadcrumb:
// Sync: data is written only before ch is closed; readers range ch.
// Sync: mu protects m and n together.
// Sync: ready atomic publishes immutable cfg pointer.If you cannot write one sentence naming the HB edge, the design is probably wrong.
Worked example — broken then proven
Broken:
var a, b int
func f() {
a = 1
b = 1
}
func g() {
for b == 0 {
runtime.Gosched()
}
fmt.Println(a) // may print 0: race + no HB
}Fixed with channel:
done := make(chan struct{})
go func() {
a = 1
b = 1
close(done)
}()
<-done
fmt.Println(a, b) // 1 1Worked example — WaitGroup as publication
var data int
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
data = 7
}()
wg.Wait()
fmt.Println(data) // 7 — Done HB WaitLab 1 — Annotate old code
Workspace: ~/lab/90daysofx/01-go/day28
- Reopen Day 19–27 labs.
- Add
// Sync:comments on every shared variable.
- If you cannot annotate, rewrite until you can.
go test -race ./...Lab 2 — Prove one race you fixed
Write doc/race-story.md (or journal section) with:
- The racy snippet
- Race detector excerpt
- The HB edge you introduced
- Why sleep was not an acceptable fix
Lab 3 — Publication patterns matrix
Implement three versions of “publish config to readers”:
| Version | Mechanism |
|---|---|
| A | Mutex around config struct |
| B | Channel close after write |
| C | atomic.Pointer to immutable config |
Table tests: many readers, occasional writers. Race-clean. Journal tradeoffs (latency, API shape).
Lab 4 — Read the official model (targeted)
Open https://go.dev/ref/mem. Skim:
- Introduction
- Synchronization (channels, mutexes)
- Incorrect synchronization
Write five bullet takeaways in your own words—no paste.
Common gotchas
| Gotcha | Fix |
|---|---|
| Double-checked locking without atomics/fences | Use Once or proper atomic publish |
| Reading fields after free-unlock copy mistakes | Keep lock scope correct |
| Believing race detector is the memory model | Detector implements checks; model is the law |
| “Single writer so no sync needed” for readers | Readers still need HB with writer |
| Over-using atomics for complex state | Mutex or channel ownership |
Checkpoint
- Defines happens-before in plain language
- Lists ≥4 Go primitives that create HB edges
- Documented Sync comments on a real lab
- Wrote a race-fix story with evidence
- Compared three publication patterns
- All day28 tests pass with
-race
Commit
git add .
git commit -m "day28: memory model lite + sync annotations + race story"Write three personal gotchas before continuing.
Tomorrow
Day 29 — Concurrency integration: combine pipeline + context + tests + race-clean design into one mini-tool.
Day 29 — Concurrency integration
Stage III · ~3h (theory-heavy + build)
Goal: Integrate Stage III skills into one race-clean concurrent tool: pipeline or pool + context + tests + documented synchronization—ready for tomorrow’s gate.
Integration day is not “new primitives.” It is composition under constraints: cancellation, bounds, errors, tests, and readability.
Why this day exists
Isolated days create isolated muscles. Production code needs:
- Bounded work
- Cooperative cancel
- Observable errors
- Tests that fail if races return
- A README a teammate can run
Day 30 grades this composition. Build it seriously today.
Theory 1 — Choose a product shape
Pick one mini-tool (do not boil the ocean):
| Option | Description |
|---|---|
| A. Concurrent line processor | Read paths/URLs/strings; transform with W workers; print/json results |
| B. Site prober | HEAD/GET list of URLs with limit + timeout; summarize codes |
| C. Directory walker pipeline | Enumerate files → hash/count → report (careful with real FS size) |
All options must include:
contexttimeout or cancel
- Bounded concurrency (
errgroup.SetLimitor pool)
- Race-clean tests
- Non-zero exit on hard failure mode
Theory 2 — Package layout for a tiny tool
day29/
go.mod
README.md
cmd/tool/main.go # flags, os.Exit
internal/engine/ # concurrent core
internal/engine/engine_test.go
Rules:
- Keep concurrency out of
mainas much as possible—testengine
- Flags parse in
main; pure functions take structs
- No global mutable state
Theory 3 — API sketch (engine)
package engine
type Config struct {
Workers int
Timeout time.Duration
// Mode: fail-fast vs best-effort
}
type Item struct {
ID string
// payload
}
type Result struct {
ID string
Err error
// fields
}
func Run(ctx context.Context, cfg Config, items []Item) ([]Result, error)Implementation preference for Stage III:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(cfg.Workers)
// per item: g.Go(...)Or channel pipeline if you want streaming—document why.
Theory 4 — Testing concurrent engines
Deterministic unit tests
- Fake work function injected (
Processorinterface or func field)
- No real network in unit tests (use
httptestif HTTP)
Race tests
go test -race -count=10 ./...Cancel tests
ctx, cancel := context.WithCancel(context.Background())
// start Run in goroutine with slow processor
cancel()
// assert returns quickly with context.Canceled or wrappedLimit tests
Atomic in-flight counter under fake processor sleep—max ≤ workers.
Theory 5 — CLI ergonomics (minimal)
tool -workers 8 -timeout 3s inputs.txt
- Default workers:
runtime.GOMAXPROCS(0)or 8
- Timeout: wraps root context
- Signals (optional stretch):
signal.NotifyContext
Print summary to stderr; machine-readable results optional.
Theory 6 — Definition of done (integration)
| Check | Pass criteria |
|---|---|
| Race | -race -count=10 clean |
| Cancel | Timeout stops work |
| Bound | Never exceeds workers in-flight |
| Errors | Fail-fast or documented best-effort |
| Sync docs | // Sync: or design note in README |
| UX | README with run instructions |
Worked skeleton — engine with errgroup
package engine
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
type Config struct {
Workers int
}
type Item struct{ N int }
type Result struct {
N int
Square int
}
func Run(ctx context.Context, cfg Config, items []Item, fn func(context.Context, Item) (Result, error)) ([]Result, error) {
if cfg.Workers < 1 {
return nil, fmt.Errorf("workers must be >= 1")
}
out := make([]Result, len(items))
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(cfg.Workers)
for i, it := range items {
i, it := i, it
g.Go(func() error {
res, err := fn(ctx, it)
if err != nil {
return err
}
out[i] = res // exclusive index — Sync: each i written once
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return out, nil
}Lab 1 — Scaffold
Workspace: ~/lab/90daysofx/01-go/day29
mkdir -p ~/lab/90daysofx/01-go/day29/{cmd/tool,internal/engine}
cd ~/lab/90daysofx/01-go/day29
go mod init example.com/day29
go get golang.org/x/sync/errgroupWrite README goals in 10 lines before coding.
Lab 2 — Implement core + tests
- Implement
Runwith injection point for work function.
- Table tests for happy path.
- Test fail-fast error.
- Test cancel.
- Test worker limit with atomic in-flight.
go test -race -count=10 ./...Lab 3 — CLI wiring
cmd/tool/main.go:
- Parse flags
- Build items from args or file
context.WithTimeout
- Print results / exit codes
go run -race ./cmd/tool -- -workers 4 -timeout 2s ...Lab 4 — Chaos pass
- Set timeout shorter than work—expect clean cancel.
- Raise workers to 1 vs 64—behavior correct both ways.
- Empty input—returns nil error and empty results.
- Deliberately introduce a race; confirm
-racecatches; revert.
Common gotchas
| Gotcha | Fix |
|---|---|
Testing only main |
Extract engine package |
| Global http.Client without timeout | Use ctx on requests + client timeouts |
Ignoring SetLimit |
Bound always |
| Partial results on error undefined | Document: nil results vs partial slice |
| README missing | Gate day will require it |
Checkpoint
- One integrated tool/module exists
- Context + bounds + errors designed deliberately
go test -race -count=10 ./...clean
- Cancel/timeout demonstrated
- README run steps work from clean clone path
- Sync strategy documented
Commit
git add .
git commit -m "day29: concurrency integration — engine + CLI + race tests"Write three personal gotchas before continuing.
Tomorrow
Day 30 — Stage III gate: formal self-check—race-clean concurrent tool with graceful shutdown/cancel and oral/written explanations of GMP, channels, and context.
Day 30 — Stage III gate
Stage III · Gate day · ~3h
Goal: Prove concurrency competence: a race-clean concurrent tool with cancellation, bounded workers, tests, and clear explanations of the Stage III mental model.
Gates are pass/fail against a rubric, not calendar guilt. If something fails, fix it today or schedule a re-gate before Stage IV.
Why this day exists
Stage III (Days 19–29) stacked:
| Day | Skill |
|---|---|
| 19 | Goroutines, GMP lite, join |
| 20 | Channels, close ownership |
| 21 | Select, timeouts |
| 22 | Mutex, atomic, WaitGroup |
| 23 | Worker pools, pipelines |
| 24 | Context cancel/deadlines |
| 25 | Race detector, leaks |
| 26 | Patterns catalog |
| 27 | errgroup |
| 28 | Memory model lite |
| 29 | Integration build |
Isolated labs create isolated muscles. The gate asks whether these compose under pressure—not whether you memorized slogans. Generics, fuzzing, and I/O depth in Stage IV will multiply concurrent bugs if your mental model is still hand-wavy.
Theory 1 — Gate artifact (definition of done)
You need a single module (Day 29 project refined is ideal) that:
- Performs concurrent work on ≥ 20 items
- Bounds concurrency (pool or
errgroup.SetLimit)
- Accepts a root
context(timeout flag and/or cancel)
- Propagates errors intentionally (fail-fast or best-effort—documented)
- Passes
go test -race -count=20 ./...on Go 1.26
- Ships README with: purpose, run, test, design notes (sync + cancel)
Stretch (not required, impressive)
signal.NotifyContextfor Ctrl-C
- Structured logging of worker IDs (
slog)
- Benchmark with/without race detector noted
- Metrics counter for completed jobs (even a simple atomic)
Explicit non-goals
- No HTTP server requirement (that is Stage V+)
- No database
- No perfect CLI UX framework
- No “must use every primitive from Days 19–28”
Theory 2 — What “race-clean under pressure” means
A single green go test -race is weak evidence. Flaky races show up under:
- Higher
-count
- Parallel tests (
t.Parallel)
- Contended maps without mutex
- Closing channels from the wrong side
Gate standard:
go test -race -count=20 ./...If it fails once in twenty, it fails the gate. Fix the happens-before edge; do not paper over with time.Sleep.
Mini mental model (must be explainable)
| Concept | One-liner you must own |
|---|---|
| Data race | Concurrent write/write or write/read without sync |
| Happens-before | Channel, mutex, WaitGroup, atomics, etc. create edges |
| Cooperative cancel | ctx.Done() observed; work stops voluntarily |
| Bound | Limit in-flight work so you cannot create unbounded goroutines |
Theory 3 — Oral/written explanation board
Answer in writing (half page each) in GATE.md. Use your project as the example, not textbook prose alone.
A. GMP
Explain G, M, P and what GOMAXPROCS limits. One misconception you no longer hold (e.g. “more goroutines always means more cores used”).
B. Channel ownership
Who closes? What happens on send-to-closed? When is a nil channel useful in a select?
C. Context
Why cancellation is cooperative. Why defer cancel() matters. When not to use context values for optional config.
D. Race
Define data race. Paste or paraphrase one race you fixed and the happens-before edge you introduced (mutex, channel send/receive, atomic, wait).
E. Mutex vs channel vs errgroup
One sentence each for when you pick them in this project.
F. Error policy (added for gate clarity)
Fail-fast (cancel siblings, return first error) vs best-effort (collect errors, continue). Quote which you chose and why.
Theory 4 — Rubric (score yourself)
| Criterion | Weight | 0 | 1 | 2 |
|---|---|---|---|---|
Race-clean under -count=20 |
25% | fails | flaky | solid |
| Bounded concurrency | 15% | unbounded | informal | explicit limit |
| Context cancel works | 15% | none | partial | demonstrated |
| Error policy clear | 10% | silent | ad-hoc | documented |
| Tests quality | 15% | none | smoke | cancel+limit+happy |
| Explanations A–F | 15% | missing | shallow | crisp |
| README / UX | 5% | none | minimal | clear |
Pass: ≥ 80% weighted with no zero in race-clean or explanations.
Self-score honestly in GATE.md:
## Rubric
| Criterion | Score (0–2) | Notes |
|-----------|-------------|-------|
| Race-clean | 2 | count=20 green |
| Bounded concurrency | 2 | SetLimit(8) |
| Context cancel | 2 | -timeout works |
| Error policy | 1 | documented late |
| Tests | 2 | cancel + happy |
| Explanations | 2 | A–F written |
| README | 2 | demo script OK |
| **Weighted** | **≈ 95%** | PASS |Theory 5 — Graceful shutdown checklist
For the gate tool (CLI/worker, not full HTTP drain):
- Timeout flag or signal cancels root context
- Workers observe
ctx.Done()on each unit of work
Wait/ errgroup / join completes after cancel
- No permanent hang on empty input or immediate error
- Process exits with meaningful code (
0success, non-zero hard fail)
“Graceful” here means cooperative cancel + join, not Stage V+ http.Server.Shutdown.
Pattern sketch (fail-fast pool)
func Run(ctx context.Context, jobs []Job, limit int) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(limit)
for _, job := range jobs {
job := job
g.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
return process(ctx, job)
})
}
return g.Wait()
}Pattern sketch (best-effort with mutex)
var (
mu sync.Mutex
errs []error
)
// each worker:
if err := process(ctx, job); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
// after WaitGroup: join errs with errors.JoinDocument which shape you shipped.
Theory 6 — Anti-patterns that fail the gate
| Anti-pattern | Why it fails |
|---|---|
| Sleep-based “sync” | Hides races; flaky under load |
Ignoring -race failures |
Gate criterion is race-clean |
Unbounded go in a loop on the production path |
OOM / scheduler thrash |
| Closing channels from multiple goroutines unsafely | Panic / races |
| Panic as control flow | Unreadable; recover is not a strategy |
Giant untested main |
No evidence under -count=20 |
| Sharing a map without mutex or confinement | Classic race |
Forgetting defer cancel() |
Context leak; children outlive intent |
Theory 7 — Minimum test matrix
Your tests should cover at least:
| Case | Intent |
|---|---|
| Happy path | ≥20 jobs complete correctly |
| Cancel / timeout | Work stops; no hang; returns context error or wrapped |
| Bound smoke | Limit is set (even if hard to assert exactly) |
| Empty input | No deadlock |
func TestRun_Cancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := Run(ctx, makeJobs(50), 4)
if !errors.Is(err, context.Canceled) && err != nil {
// accept wrapped cancel depending on design
t.Logf("err=%v (document policy)", err)
}
}Prefer asserting no hang with t.Deadline / short timeout over perfect error equality if your design aggregates.
Lab 1 — Harden Day 29 into gate shape
Workspace: Day 29 module or ~/lab/90daysofx/01-go/day30
- Confirm
go 1.26ingo.modandgo version.
- Re-run full tests with race, count 20.
- Add any missing cancel/limit/empty-input tests.
- Fix README gaps (flags, examples, error policy paragraph).
- Record rubric scores honestly in
GATE.md.
go version # go1.26.x
go test -race -count=20 ./...
go vet ./...Lab 2 — Live demo script
Write DEMO.md (or scripts/demo.sh) with commands a stranger can run:
# happy — process enough work to exercise the pool
go run ./cmd/tool --workers 4 --input ./testdata/jobs.txt
# timeout — must exit promptly
go run ./cmd/tool -timeout 1ms --workers 8 --input ./testdata/jobs.txt
# tests
go test -race -count=20 ./...Run the demo once end-to-end without editing mid-flight. If a command fails, fix code/docs, then re-run the full script.
Lab 3 — Explanation document
Create GATE.md with answers A–F and the rubric table. Timebox 45 minutes. Clarity > length. Every answer should mention a concrete file or type in your module.
Template:
# Stage III Gate — day30
Date:
Module path:
Go version:
## Artifact
- Binary/cmd:
- Bound mechanism:
- Cancel mechanism:
- Error policy: fail-fast | best-effort
## Explanations
### A. GMP
...
### B. Channel ownership
...
### C. Context
...
### D. Race fixed
...
### E. Mutex vs channel vs errgroup
...
### F. Error policy
...
## Rubric
...
## Appendix — race detector drillLab 4 — Deliberate regression drill
- Break a synchronization on purpose (e.g. remove a mutex around a shared counter).
- Show
-racefailing; capture output snippet.
- Restore the fix.
- Note the detector output in
GATE.mdappendix.
This proves you can use the detector under stress—not that you never introduce races.
# after deliberate break:
go test -race ./internal/... -count=5
# restore, re-run count=20Lab 5 — Exit codes and UX polish (short)
| Scenario | Expected exit |
|---|---|
| All jobs OK | 0 |
| Hard failure / fail-fast error | non-zero |
| Cancel via timeout | non-zero (or documented zero—be consistent) |
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
// or: context.WithTimeout from -timeout flag
if err := realMain(ctx); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Hour-by-hour suggestion
| Time | Focus |
|---|---|
| 0:00–0:30 | Inventory Day 29 gaps vs gate bar |
| 0:30–1:15 | Harden code + tests (race, cancel) |
| 1:15–1:45 | DEMO.md dry run |
| 1:45–2:30 | GATE.md explanations A–F |
| 2:30–2:50 | Regression drill + rubric |
| 2:50–3:00 | Commit; list Stage IV risks |
Common gotchas
| Gotcha | Fix |
|---|---|
| “Works on my machine” once | -count=20 with -race |
| README outdated flags | Re-run demo from zero |
| Explanations copy docs | Use your project as example |
| Partial gate | Schedule re-sit; don’t enter Stage IV blind |
| Timeout too long in tests | Use short WithTimeout in unit tests |
errgroup without SetLimit |
Add explicit bound |
| Logging from many goroutines without care | slog is safe; still avoid huge allocations in hot paths |
| Treating cancel as optional | Gate requires demonstrated cancel |
Checkpoint
- Rubric ≥ 80% with no zero on race-clean or explanations
go test -race -count=20 ./...pass on Go 1.26
- Cancel/timeout demonstrated in demo
GATE.mdexplanations A–F complete
- DEMO.md / demo script commands work cold
- Regression drill appendix present
- Honest list of remaining weaknesses
Commit
git add .
git commit -m "day30: stage III gate — race-clean concurrent tool + GATE notes"Write three personal gotchas before continuing.
Tomorrow
Stage IV begins — Day 31 Generics: type parameters and constraints after you have felt real duplication pain in concurrent and sequential code. Enter Stage IV only with a race-clean mental model—or schedule a re-gate first.