Time, Clocks, and Timers
Time, Clocks, and Timers
Overview
Systems programs schedule work, measure latency, and enforce deadlines. Go’s time package is enough for almost all of this—if you know wall clock vs monotonic behavior and avoid classic timer leaks.
Wall clock vs monotonic
t1 := time.Now()
// ... work ...
elapsed := time.Since(t1) // uses monotonic reading when possibletime.Now() carries both wall and monotonic components on modern Go. Subtraction (Since, Sub) prefers monotonic—so NTP steps don’t invent negative durations mid-measurement.
| Need | Use |
|---|---|
| Logs, certs, file mtimes | Wall clock (Format, Truncate) |
| Timeouts, RTT, deadlines | Monotonic-friendly (Since, Until, context + timer) |
| “Run at 03:00 wall” | Wall clock + recompute after sleep |
Never store only UnixNano() for elapsed timing across clock steps if you care about accuracy—prefer time.Time from Now().
Sleep and timers
time.Sleep(100 * time.Millisecond) // blocks goroutine
// prefer for cancelable wait:
t := time.NewTimer(100 * time.Millisecond)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
// fired
}Stop and drain
if !t.Stop() {
select {
case <-t.C:
default:
}
}Failing to stop/drain can wake a select later and cause subtle double-fire bugs.
Ticker
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
select {
case <-ctx.Done():
return
case <-tk.C:
// periodic work — keep it shorter than period
}
}Slow handlers + ticker = tick pile-up (channel buffer size 1 drops intermediate ticks in recent Go for Ticker—still don’t do multi-second work on every tick without backpressure).
Deadlines with context
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
// pass ctx to Dial, Query, HTTPCompose: signal cancel + timeout cancel (NotifyContext outer, timeout inner or vice versa).
Parsing and formatting
t, err := time.Parse(time.RFC3339, "2026-08-04T12:00:00Z")
s := t.UTC().Format(time.RFC3339Nano)For CLI flags: flag.Duration already parses 300ms, 2s, 1h.
File mtimes and caching
st, err := os.Stat(path)
mod := st.ModTime()
if mod.After(lastLoad) {
// reload
}Clock skew across NFS nodes can make mtime comparisons surprising—prefer content hashes for critical caches.
Minimal tool: deadline
// run command with max wall time
ctx, cancel := context.WithTimeout(context.Background(), *max)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
err := cmd.Run()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("timeout after %s", *max)
}Rules of thumb
| Do | Don’t |
|---|---|
Since for latency |
Compare wall Unix() deltas for RTT |
Stop timers you create |
Leak time.After in tight loops |
| Bound periodic work | Do unbounded IO every tick without skip/logic |
| UTC in logs/storage | Mix local TZ without documenting |
Try next
- Measure a sleep with
Sincewhile changing system time (VM lab)—note stability. - Rewrite a
time.Afterloop toNewTimer+Reset. - Build
deadline 2s -- sleep 10and confirm kill viaCommandContext.