Time: Throttle, Backpressure, Timeouts
Time: Throttle, Backpressure, Timeouts
Overview
Time tools in concurrent Go: limit parallelism, reject overload, bound wait, and schedule delayed work—without leaking timers.
Throttle (≤ N concurrent)
Buffered channel as semaphore:
sema capacity N:
handle: sema <- token // acquire
go work(); <-sema // release in worker
wait: fill all slots again to ensure idle
func throttle(n int, fn func()) (handle func(), wait func()) {
sema := make(chan struct{}, n)
handle = func() {
sema <- struct{}{}
go func() {
fn()
<-sema
}()
}
wait = func() {
for i := 0; i < n; i++ {
sema <- struct{}{} // re-acquire all slots
}
}
return handle, wait
} throttle (≤ N):
acquire slot → run work → release
full? → block
backpressure:
full? → return "busy" immediately (select default)
With N=2 and 4×100ms jobs → ~200ms total.
Backpressure (fail fast)
handle = func() error {
select {
case sema <- struct{}{}:
go func() {
fn()
<-sema
}()
return nil
default:
return errors.New("busy")
}
}select + default:
ready case → take it
else → default immediately (no wait)
Clients must handle "busy" (retry later, shed load, degrade).
Operation timeout with select
func withTimeout(timeout time.Duration, fn func() int) (int, error) {
done := make(chan struct{})
var result int
go func() {
result = fn()
close(done)
}()
select {
case <-done:
return result, nil
case <-time.After(timeout):
return 0, errors.New("timeout")
}
}sequence (top → bottom):
actors: caller, worker, timer
caller --> worker : start fn
caller --> timer : After(timeout)
worker --> caller : done
timer --> caller : fire
note: alt fn finishes first
note: else timeout first
note: return error; worker may still run!
Caveat: on timeout the worker goroutine may continue unless fn also observes cancel (prefer context—next chapter).
Timers vs time.After
| API | Notes |
|---|---|
time.After |
Convenient; new timer every call—bad in hot loops |
time.NewTimer |
Reuse with Stop/Reset carefully |
time.AfterFunc |
Run function after delay; Stop cancels |
timer := time.NewTimer(100 * time.Millisecond)
defer timer.Stop()
select {
case <-timer.C:
// fired
case <-cancel:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}Hot path anti-pattern:
for {
select {
case <-in:
case <-time.After(time.Hour): // allocates every iteration
}
}Runnable example
go mod init example && go run .package main
import (
"errors"
"fmt"
"time"
)
func work() { time.Sleep(80 * time.Millisecond) }
func main() {
sema := make(chan struct{}, 2)
handle := func() error {
select {
case sema <- struct{}{}:
go func() {
work()
<-sema
}()
return nil
default:
return errors.New("busy")
}
}
for i := 1; i <= 4; i++ {
fmt.Println(i, handle())
}
time.Sleep(200 * time.Millisecond)
}Expected: first two nil, next two busy.
Try next: Replace timeout demo with context.WithTimeout so the worker can exit early.