Timeouts
Bound how long you wait for a channel operation using time.After or context.WithTimeout inside a select.
Timeouts in Go are built on select and channels. time.After(d) returns a channel that sends after duration d - combine it with a select to bail out if an operation takes too long.
Wait up to one second for a result. If the result does not arrive in time, the time.After case fires and the program continues without it.
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}
c2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("timeout 2")
}
}For cancellable timeouts - where you want to clean up the timer if the operation succeeds - use time.NewTimer with explicit Stop:
timer := time.NewTimer(2 * time.Second)
defer timer.Stop() // cancel if we return early
select {
case result := <-work:
fmt.Println("got result:", result)
case <-timer.C:
fmt.Println("timed out")
}For network requests and service calls, context.WithTimeout is the standard approach - it propagates the deadline through the entire call stack:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// errors.Is(err, context.DeadlineExceeded) is true on timeout
fmt.Println("request failed:", err)
return
}
defer resp.Body.Close()In production
Always set timeouts on outbound network calls. The zero-timeout default in net/http has caused countless production incidents where a slow downstream service held goroutines open indefinitely. Use http.Client{Timeout: ...} or wrap requests in a context with deadline via context.WithTimeout. time.After leaks a timer if the channel is never drained and the timeout never fires - prefer time.NewTimer with Stop() in long-running loops or hot paths to avoid timer accumulation.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free