time.AfterFunc — Tasks¶
Eighteen hands-on exercises, ordered from beginner to advanced. Each includes a brief description, requirements, hints, and tests you should pass.
Task 1: Hello, AfterFunc¶
Goal: Print "hello after 1 second" once.
Requirements:
- Use
time.AfterFunc. - Block
mainuntil the callback runs.
Hint: Use a channel.
Test: Run; output should be "hello after 1 second" once, then exit.
Task 2: Cancellable countdown¶
Goal: Schedule a callback after a duration; allow cancellation.
Requirements:
- Function
Countdown(d time.Duration, fn func()) (cancel func() bool). cancel()returns true if the callback was prevented, false otherwise.
Hint: Use the timer's Stop.
Test: Call Countdown, then immediately cancel. Verify fn did not run.
Task 3: Debounced echo¶
Goal: Build a debouncer that prints "settled" once after 100 ms of quiet.
Requirements:
- Method
Trigger()to indicate input. - After 100 ms of no Trigger, callback fires.
- New Triggers reset the timer.
Hint: Store *time.Timer in a struct; Stop and re-create on each Trigger.
Test: Call Trigger 5 times rapidly. Verify "settled" prints once, ~100 ms after the last Trigger.
Task 4: Idle connection timer¶
Goal: Close a mock connection after 200 ms of inactivity.
Requirements:
Connstruct withTouch()andClose()methods.- After 200 ms without Touch, the conn closes itself.
- Explicit Close also works.
Hint: Use Reset on Touch.
Test: Create conn. Call Touch every 100 ms for a second. Verify it's still open. Stop calling Touch. Verify it closes ~200 ms later.
Task 5: Watchdog¶
Goal: Detect 30 seconds of inactivity in a long-running task.
Requirements:
WatchdogwithTouch(),Stop(), and a fire callback.- Fire callback runs once, even if multiple touches race.
Hint: Use sync/atomic for the "fired" flag.
Test: Touch every 10 seconds for 1 minute. Verify no fire. Stop touching. Verify fire within 30 seconds.
Task 6: Deadline race¶
Goal: Race a result against a deadline.
Requirements:
- Function
WithDeadline(d time.Duration, op func() string) (string, error). - Returns op's result if it finishes in time, otherwise
errors.New("timeout").
Hint: Use a buffered channel of size 1.
Test: With slow op (500 ms) and short deadline (100 ms), return timeout. With fast op (50 ms) and longer deadline (200 ms), return op's result.
Task 7: Self-rescheduling job¶
Goal: Run a job every second, but skip if the previous instance is still running.
Requirements:
- Function
RunPeriodic(d time.Duration, fn func(), stop <-chan struct{}). - Stops when
stopcloses.
Hint: Atomic flag for "running".
Test: Verify jobs don't overlap. Verify clean stop on signal.
Task 8: Bounded retry¶
Goal: Retry an operation with exponential backoff, max 5 attempts.
Requirements:
- Function
Retry(op func() error) error. - Returns nil if op succeeds; returns final error if all attempts fail.
- Use jittered exponential backoff.
Hint: Use AfterFunc for each delay; coordinate via a result channel.
Test: Op that fails 4 times then succeeds: returns nil. Op that always fails: returns error after ~5 backoff steps.
Task 9: TTL cache with one timer per entry¶
Goal: Cache with per-key TTL using individual timers.
Requirements:
CachewithSet(k, v, ttl),Get(k),Delete(k).- After
ttl, the key is removed automatically.
Hint: Map of *time.Timer keyed by string. Stop previous timer on overwrite.
Test: Set ~100 keys with various TTLs. Verify they expire at the right times. Verify explicit delete stops the timer.
Task 10: TTL cache with single sweeper¶
Goal: Same as Task 9, but use one sweeper timer for the whole cache.
Requirements:
- One
*time.Timerfor the whole cache, set to the earliest expiration. - On fire, scan and remove all expired entries; reschedule.
Hint: Track expirations separately from values; rearm after each fire.
Test: Compare memory usage vs Task 9 at 10K entries.
Task 11: context.AfterFunc cleanup chain¶
Goal: Register multiple cleanups for a context, running them in reverse order on cancel.
Requirements:
CleanerwithDefer(fn func())andRun(ctx context.Context).- On ctx cancel, all deferred fns run in LIFO order.
Hint: Use context.AfterFunc(ctx, ...) to register a single function that runs all the stored cleanups.
Test: Defer 3 cleanups (printing "a", "b", "c"). Cancel ctx. Verify output is "c b a".
Task 12: Per-request deadline middleware¶
Goal: HTTP middleware that enforces a 5-second deadline per request.
Requirements:
- Wraps an
http.Handler. - Returns 504 Gateway Timeout if the inner handler exceeds 5 seconds.
Hint: context.WithTimeout. Run handler in goroutine; race on a channel.
Test: Slow handler (10s) returns 504. Fast handler returns the normal response.
Task 13: Deferred email scheduler¶
Goal: Schedule an "email" (just a log line) to fire at a specific time.
Requirements:
Schedule(at time.Time, recipient string).- Cancel via
Cancel(id). - Survive restart? (bonus — persist to disk).
Hint: time.AfterFunc(time.Until(at), ...).
Test: Schedule 5 emails at various times. Verify they fire in order. Cancel one. Verify others still fire.
Task 14: Mocked-clock debouncer¶
Goal: Same debouncer as Task 3, but with a Clock interface for testing.
Requirements:
- Inject a
Clockinterface withAfterFunc(d, fn) Timer. - Production: real time. Tests: fake clock with
Advance(d).
Hint: Define minimal Clock interface. Write a realClock and a fakeClock.
Test: Test the debouncer with the fake clock, advancing time manually. No time.Sleep in tests.
Task 15: Earliest-deadline scheduler¶
Goal: Schedule N jobs at various times, using only one runtime timer.
Requirements:
SchedulerwithSchedule(at time.Time, fn func())andCancel(id) bool.- Internally use a single
*time.Timerplus a user-space min-heap.
Hint: When the timer fires, process all due entries, then rearm for the next-earliest.
Test: Schedule 1000 jobs. Verify all fire at correct times. Verify only one runtime timer is created (count via metrics).
Task 16: Observable AfterFunc wrapper¶
Goal: Wrap time.AfterFunc with metrics.
Requirements:
- Function
AfterFunc(purpose string, d time.Duration, fn func())that updates counters. - Counters: created, fired, stopped, panic, live (gauge), latency (histogram).
- Recovers panics.
Hint: Embed the wrapper in a struct; provide Stop and Reset methods.
Test: Use the wrapper; verify metrics update correctly. Inject a panic; verify counter increments.
Task 17: Stop-vs-fire race stress test¶
Goal: Stress-test a Stop-vs-fire race; verify your code handles it.
Requirements:
- A component (debouncer, watchdog, or similar) you've built.
- A test that hammers it concurrently for 1 second.
- Verify no double-execution, no panic, no data race.
Hint: Run with -race. Use atomic counters for "fired" check.
Test: Run; should pass cleanly with -race.
Task 18: Hashed timing wheel¶
Goal: Implement a simple single-level hashed timing wheel.
Requirements:
- Configurable tick width and number of buckets.
- O(1) Schedule and Cancel.
- Tick callback fires all entries in the current bucket.
Hint: Doubly-linked list per bucket; pointer-based remove.
Test: Schedule 10K entries with random delays in [0, max). Verify all fire within their bucket window.
Bonus tasks¶
B1. Adaptive backoff retry¶
Extend Task 8 to learn from previous attempts: on persistent failure, increase the initial backoff; on success, decrease.
B2. Distributed rate limit¶
Build a rate limiter that coordinates across processes via Redis TTLs. Use time.AfterFunc only for local cleanup.
B3. Cron-like scheduler¶
Parse a cron expression and use time.AfterFunc to fire at the right times. Handle DST, leap seconds.
B4. Test framework integration¶
Integrate your Clock interface with testing.T so tests automatically use a mocked clock when run via go test.
B5. Profiling tool¶
Build a tool that profiles a Go binary and reports timer-related metrics: live count, churn rate, top creation sites.
How to verify your solutions¶
For each task:
- Run with
go test -race ./.... - Run with
-cpuprofile=cpu.outand inspect for hot spots. - Run with
-memprofile=mem.outand inspect for leaks. - Write tests using a mocked clock for determinism.
If your test sometimes fails on CI but not locally, you likely have a race. Run with -race -count=100 to flush it out.
Reference solutions¶
Reference implementations for each task are in the parent repository under examples/afterfunc/. They are not the only correct solutions; many approaches work.
When comparing your solution to the reference, ask:
- Does mine handle all the edge cases?
- Is the API as clean?
- What did I miss?
- What did I do better?
Critical reading of reference solutions is half the learning.
Solution outlines¶
Below are brief outlines for each task. Treat as guidance; the actual implementation is yours.
Task 1 outline¶
done := make(chan struct{})
time.AfterFunc(time.Second, func() {
fmt.Println("hello after 1 second")
close(done)
})
<-done
Task 2 outline¶
func Countdown(d time.Duration, fn func()) (cancel func() bool) {
t := time.AfterFunc(d, fn)
return t.Stop
}
Task 3 outline¶
type Debouncer struct {
mu sync.Mutex
t *time.Timer
d time.Duration
fn func()
}
func (db *Debouncer) Trigger() {
db.mu.Lock()
defer db.mu.Unlock()
if db.t != nil { db.t.Stop() }
db.t = time.AfterFunc(db.d, db.fn)
}
Task 4 outline¶
type Conn struct {
mu sync.Mutex
t *time.Timer
timeout time.Duration
closed bool
}
func New(timeout time.Duration) *Conn {
c := &Conn{timeout: timeout}
c.t = time.AfterFunc(timeout, c.fire)
return c
}
func (c *Conn) Touch() { /* Reset */ }
func (c *Conn) Close() { /* idempotent close */ }
func (c *Conn) fire() { /* close path */ }
Task 5 outline¶
Task 6 outline¶
func WithDeadline(d time.Duration, op func() string) (string, error) {
type r struct{ v string; err error }
out := make(chan r, 1)
t := time.AfterFunc(d, func() { out <- r{err: errors.New("timeout")} })
defer t.Stop()
go func() { out <- r{v: op()} }()
res := <-out
return res.v, res.err
}
Task 7 outline¶
func RunPeriodic(d time.Duration, fn func(), stop <-chan struct{}) {
var running atomic.Bool
var tick func()
tick = func() {
if !running.CompareAndSwap(false, true) { return }
defer running.Store(false)
select {
case <-stop: return
default:
}
fn()
time.AfterFunc(d, tick)
}
time.AfterFunc(d, tick)
}
Task 8 outline¶
func Retry(op func() error) error {
var attempt int
backoff := 100 * time.Millisecond
for {
if err := op(); err == nil {
return nil
}
attempt++
if attempt >= 5 {
return errors.New("retries exhausted")
}
time.Sleep(jitter(backoff))
backoff *= 2
}
}
(Using a synchronous loop for simplicity. For an async version, use AfterFunc and a result channel.)
Task 9 outline¶
type Cache struct {
mu sync.Mutex
items map[string]string
timers map[string]*time.Timer
}
func (c *Cache) Set(k, v string, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
if t, ok := c.timers[k]; ok { t.Stop() }
c.items[k] = v
c.timers[k] = time.AfterFunc(ttl, func() {
c.mu.Lock()
delete(c.items, k)
delete(c.timers, k)
c.mu.Unlock()
})
}
Task 10 outline¶
type Cache struct {
mu sync.Mutex
items map[string]entry
timer *time.Timer
}
type entry struct {
v string
expires time.Time
}
func (c *Cache) arm() {
var earliest time.Time
for _, e := range c.items {
if earliest.IsZero() || e.expires.Before(earliest) {
earliest = e.expires
}
}
if earliest.IsZero() { return }
d := time.Until(earliest)
if d < 0 { d = 0 }
if c.timer == nil {
c.timer = time.AfterFunc(d, c.sweep)
} else {
c.timer.Reset(d)
}
}
func (c *Cache) sweep() { /* delete expired, rearm */ }
Tasks 11-18 outlines¶
Similar in style. Implement; compare with reference if available.
Bonus: a self-test¶
After completing the tasks, take the self-test in interview.md. If you can answer 30+ questions confidently, you're senior-level.
Bonus: write your own task¶
Once you've completed all 18, design your own task. What pattern have you seen in production that you haven't built? Build it. The exercise of designing is itself a learning step.
End of tasks.