Skip to content

Weekly Review — Month 6 · Week 2 (Days 148–154)

Journal index · Roadmap › this week

📅 The Week in One Line

Took a working Go binary all the way to "production-shippable": tiny multi-stage images, a Compose local stack, distroless hardening, a build/test/scan CI pipeline, 12-factor config & file-based secrets, and signal-driven graceful shutdown.

✅ What I Completed

  • Day 148 — Multi-stage Docker builds; CGO_ENABLED=0 static binary; cache-friendly layer order; -ldflags version stamp
  • Day 149 — docker compose stack (app + Postgres + Redis); DNS-by-service-name; depends_on + healthcheck readiness
  • Day 150 — Image hardening; scratch vs distroless static; non-root, read-only rootfs, dropped caps; CA roots & tzdata
  • Day 151 — GitHub Actions: go vet, go test -race -cover, module cache, govulncheck; least-privilege permissions
  • Day 152 — 12-factor config; os.LookupEnv; accumulate-and-errors.Join; *_FILE secret pattern; redaction
  • Day 153 — Signals & graceful shutdown; signal.NotifyContext; srv.Shutdown(ctx); http.ErrServerClosed sentinel
  • Day 154 — Week review + recall
  • Stdlib examples: graceful, config, secrets, buildinfo
  • Exercises solved: 3 (envconfig, secretsource, drain) — all go test green

💡 Lessons Learned

  • Go's static-binary model is what makes scratch/distroless possible — CGO_ENABLED=0 is the switch that unlocks it.
  • "Started" ≠ "ready" and "stop" ≠ "stopped": healthchecks gate the first gap, graceful Shutdown the second. Both come from refusing to assume timing.
  • Fail-fast vs drain-slow is the production rhythm: be intolerant of bad config at boot, patient with in-flight work at shutdown.
  • errors.Join is the right tool for a config loader — one error value, all causes, still errors.Is-matchable.
  • http.ErrServerClosed is success, not failure — guard it with errors.Is.
  • Secrets belong in files, not env vars; env leaks through inspect, /proc, child processes, and dumps.
  • govulncheck beats naive scanners because it's reachability-based: it only flags CVEs your call graph actually touches.

💪 Strengths (what clicked)

  • The whole lifecycle now reads as one pipeline around a single binary.
  • signal.NotifyContext + goroutine-ListenAndServe + bounded Shutdown is a pattern I can write from memory.
  • Layer-cache ordering and multi-stage discipline feel natural.

🧩 Weaknesses (what's still fuzzy)

  • Draining long-lived connections (WebSocket/SSE) that Shutdown ignores.
  • BuildKit cache mounts behavior across CI runners (persistence, keys).
  • Real secret managers (Vault/CSI) vs the plain *_FILE convention.

🔁 Spaced-Repetition Re-quiz (topics from earlier weeks)

  1. Q: (Channels, M2) What are the results of send/recv/close on a nil vs a closed channel?
    A

nil channel: send and recv block forever; close panics. Closed channel: send panics, recv returns the zero value immediately with ok==false, closing again panics. 2. Q: (Errors, M3) Difference between errors.Is and errors.As?

A

errors.Is tests whether any error in the chain matches a target sentinel value. errors.As finds the first error in the chain assignable to a target type and assigns it, so you can read its fields.

3. Q: (Slices, M1) Why can append mutate a slice the caller still holds?
A

If capacity allows, append writes into the shared backing array in place, so an aliasing slice sees the change. Only when it must grow does it allocate a new array and decouple. Re-slicing/subslicing shares the same array until a reallocation.

4. Q: (Context, M5) Where does context.Context go in a function signature, and what must you never do with it?
A

It's the first parameter, conventionally named ctx. Never store it in a struct for later use, and never pass a nil context — use context.TODO() if you don't have one yet.

5. Q: (gRPC, M5 W2) Which return-value detail is required for a panic-recovery interceptor to surface an error?
A

Named return values — the deferred recover() must assign to the named err return so the panic becomes a returned error instead of crashing the server.

🎯 Action Items

  • Add a graceful-drain path for SSE/WebSocket handlers (propagate shutdown ctx).
  • Wire govulncheck as a blocking CI gate on reachable HIGH/CRITICAL only.
  • Add a Makefile/pre-push hook running go vet, go test -race -cover, govulncheck.
  • Pin base image tags by digest in the Dockerfile.

🚀 Next Week Goals

  • Observability: Prometheus-style metrics, OpenTelemetry tracing, and structured logs correlated by request/trace IDs (building on slog from Week 1).
  • Connect the graceful-shutdown lifecycle to flushing metrics/traces on exit.

📊 Metrics

Hours Days hit Exercises Commits Avg confidence
10.5 7/7 3 7 3.6/5

Suggested commit: docs(journal): month 6 week 2 review