Skip to content

Day 060 — select & time.After

Month 3 · Week 1 · ⬅ Day 059 · Day 061 ➡ · Journal index

🎯 Learning Objective

Wait on multiple channel operations at once with select, and bound any blocking operation with a timeout using time.After/time.NewTimer.

📚 Topics

  • select semantics: blocking, random choice among ready cases, default
  • time.After timeouts; time.NewTimer/time.NewTicker and why you Stop them
  • Labelled break to leave a select loop

📖 Reading / Sources

📝 Notes

  • select blocks until one of its case channel ops can proceed, then runs that one. If several are ready it chooses uniformly at random — there is no top-to-bottom priority → [[select]].
  • A default case makes select non-blocking: if no other case is ready right now, default runs. This is how you do non-blocking send/receive and polling.
  • An empty select {} blocks forever (occasionally used to park main).
  • time.After(d) returns a <-chan Time that fires once after d — the idiomatic one-shot timeout in a select. It's a thin wrapper over time.NewTimer(d).C → [[time-after]].
  • Timer leak trap: a time.After inside a loop allocates a fresh timer each pass; the old ones live until they fire. In hot loops use time.NewTimer and Stop()/Reset() it instead. (Go 1.23+ makes unreferenced time.After timers eligible for GC sooner, but the idiom still holds.)
  • time.NewTicker(d) fires repeatedly every d; always defer ticker.Stop() or it leaks → [[ticker]].
  • Plain break only exits the select, not an enclosing for. Use a labelled break (break loop) to leave the loop → [[labelled-break]].

💻 Code Examples

select {
case r := <-slow:
    fmt.Println("got:", r)
case <-time.After(20 * time.Millisecond):
    fmt.Println("timed out") // fires if slow hasn't produced in time
}

Full code: examples/month-03/select-timeout/main.go · Run: go run ./examples/month-03/select-timeout

🏋️ Exercises / Practice

Exercise Status Link
Worker pool over a jobs channel (select-driven dispatch) exercises/month-03/week-1/workerpool/

🐛 Mistakes Made

  • Used break inside a select in a for loop expecting to exit the loop — it only left the select; the loop spun forever. Added a label.
  • Put time.After in a tight loop and watched the timer count climb under -race/pprof. Switched to a single reusable time.NewTimer.

❓ Open Questions

  • When should I reach for context.WithTimeout instead of time.After? (Answer preview: when the timeout must propagate/cancel downstream calls — that's Month 3 later.)

🧠 Active Recall (answer without looking)

  1. Q: If two select cases are ready at the same time, which runs?

    A One is chosen uniformly at random — `select` has no case priority or ordering.

  2. Q: Why prefer time.NewTimer over time.After inside a loop?

    A `time.After` allocates a new timer every iteration that lives until it fires (a leak in hot loops). A `Timer` can be `Stop`/`Reset` and reused.

🪶 Feynman Reflection

select is a receptionist watching several phone lines: it waits until any line rings, picks up exactly one (flipping a coin if several ring together), and a default line means "if nobody's calling, do something else immediately." time.After is an egg-timer you add as one more line so you never wait indefinitely.

🕳️ Knowledge Gaps

  • Fairness guarantees under heavy contention across many cases — revisit with the scheduler internals.

✅ Summary

I can multiplex channel operations with select, make them non-blocking with default, and bound blocking work with time.After/Timer, escaping the loop cleanly with a labelled break.

⏭️ Next Steps / Prep for Tomorrow

  • Day 061: deadlocks — how they happen and how to spot them fast.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): select and time.After timeouts (day 060)