Context
context.WithCancel / WithTimeout / WithDeadline propagate cancellation and deadlines through the call stack - pass context as the first argument to any function that does I/O.
context.Context carries a cancellation signal, a deadline, and optional key-value metadata through a call stack. Pass it as the first argument to any function that does I/O or calls an external service - the convention is enforced by linters and expected by the entire stdlib.
context.WithCancel returns a derived context and a cancel function. Calling cancel broadcasts the cancellation to every goroutine holding the context. Always call cancel - if you forget, the parent context leaks the child until the parent itself is cancelled.
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d stopped: %v\n", id, ctx.Err())
return
case <-time.After(200 * time.Millisecond):
fmt.Printf("worker %d tick\n", id)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go doWork(ctx, 1)
go doWork(ctx, 2)
time.Sleep(600 * time.Millisecond)
cancel()
time.Sleep(100 * time.Millisecond)
}context.WithTimeout cancels the context automatically after the duration. context.WithDeadline does the same with an absolute time. Both return a cancel function - call it with defer to release resources when the operation finishes before the deadline.
package main
import (
"context"
"fmt"
"time"
)
func fetchData(ctx context.Context) (string, error) {
// Simulate a slow operation
select {
case <-time.After(2 * time.Second):
return "data", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
result, err := fetchData(ctx)
if err != nil {
fmt.Println("error:", err) // context deadline exceeded
return
}
fmt.Println("result:", result)
}context.WithValue attaches request-scoped metadata. Use a private key type to avoid collisions across packages. Retrieve values with ctx.Value(key) and assert the type.
package main
import (
"context"
"fmt"
)
type contextKey string
const traceIDKey contextKey = "traceID"
func handleRequest(ctx context.Context) {
traceID, ok := ctx.Value(traceIDKey).(string)
if !ok {
fmt.Println("no trace ID in context")
return
}
fmt.Println("handling request with trace ID:", traceID)
}
func main() {
ctx := context.WithValue(context.Background(), traceIDKey, "abc-123")
handleRequest(ctx)
}In production
Always accept context.Context as the first parameter of functions that do I/O or call external services - the convention is so universal that go vet flags violations. Never store a context in a struct field; contexts are request-scoped and must flow through the call stack, not be held across requests. Use context.WithTimeout for outbound HTTP calls, database queries, and gRPC calls - the zero-timeout default in net/http has caused countless production incidents where a slow downstream service held goroutines open indefinitely. Check ctx.Done() in long-running loops so cancellation is propagated promptly rather than only at the next I/O boundary. context.WithValue is for request-scoped metadata (trace IDs, auth info, request IDs) only - it is not a mechanism for passing optional function parameters, which belong in function signatures. Use a private unexported key type (a named type contextKey string) to avoid key collisions between packages that share a context.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free