go / sleepctx
I use context-aware sleep to respect cancellation in retry loops, polling, and rate limiting.
The problem
time.Sleep blocks unconditionally:
func poll(ctx context.Context) error {
for {
if done, err := check(); done {
return err
}
time.Sleep(5 * time.Second) // ignores ctx cancellation
}
}
If the context is canceled, the goroutine still waits the full duration before noticing.
The pattern
func sleepCtx(ctx context.Context, d time.Duration) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(d):
return nil
}
}
Returns immediately if the context is canceled, otherwise waits for the duration.
Usage
func poll(ctx context.Context) error {
for {
if done, err := check(); done {
return err
}
if err := sleepCtx(ctx, 5*time.Second); err != nil {
return err // context canceled
}
}
}
Works well in retry loops:
func fetchWithRetry(ctx context.Context, url string) (*Response, error) {
delays := []time.Duration{1, 2, 4, 8, 16}
var lastErr error
for _, delay := range delays {
resp, err := fetch(ctx, url)
if err == nil {
return resp, nil
}
lastErr = err
if err := sleepCtx(ctx, delay*time.Second); err != nil {
return nil, err
}
}
return nil, lastErr
}
When to use
- Retry loops with backoff
- Polling with jitter
- Rate limiting between operations
- Any sleep that should respect cancellation
When not to use
- One-shot delays where cancellation doesn't matter
- Inside
selectstatements (just add the ctx case directly)