If you work with Go in production, you will use the context package almost every day. It is one of the most important packages in the standard library for building reliable backend services.
In this post we will cover:
- What
context.Contextis and why it exists - Cancellation and timeouts
- Deadlines
- Request-scoped values
- Best practices and common mistakes
1. Why Does Context Exist?
In concurrent and networked applications we constantly face questions like:
- How do I cancel a long-running operation?
- How do I enforce a timeout?
- How do I pass request-scoped data (trace IDs, user IDs, etc.) without polluting function signatures?
The context package solves all of these problems in a consistent way.
A context.Context carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between goroutines.
2. Creating Contexts
You almost never create a context from scratch in application code. Instead you start from one of these:
ctx := context.Background() // empty root context
ctx := context.TODO() // when you're not sure yet (avoid in production)
In real services you usually receive a context from the framework (HTTP server, gRPC, etc.) and derive new ones from it.
3. Cancellation
The most common use of context is cancellation.
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always call cancel to release resources
go func() {
// simulate some work
time.Sleep(2 * time.Second)
cancel() // cancel the context
}()
select {
case <-ctx.Done():
fmt.Println("context was cancelled:", ctx.Err())
}
When a context is cancelled:
ctx.Done()is closedctx.Err()returnscontext.Canceled
4. Timeouts and Deadlines
WithTimeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("work finished")
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err()) // context.DeadlineExceeded
}
WithDeadline
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
WithTimeout is just a convenience wrapper around WithDeadline.
5. Request-Scoped Values
You can attach values to a context:
type key string
const userIDKey key = "userID"
ctx := context.WithValue(context.Background(), userIDKey, "12345")
userID := ctx.Value(userIDKey).(string)
fmt.Println(userID) // 12345
Warning: Use context values sparingly. They are intended for request-scoped data (trace IDs, auth info, etc.), not for passing optional parameters.
6. Practical Example — HTTP Handler with Timeout
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
result, err := doExpensiveWork(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, result)
}
func doExpensiveWork(ctx context.Context) (string, error) {
select {
case <-time.After(5 * time.Second):
return "done", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
7. Best Practices
Do
- Always pass
context.Contextas the first parameter of a function. - Always call the
cancelfunction (usedefer cancel()). - Prefer
context.WithTimeout/WithDeadlineover manual timers. - Check
ctx.Err()or listen onctx.Done()in long-running operations.
Don’t
- Don’t store contexts inside structs (pass them explicitly).
- Don’t use context values for optional parameters.
- Don’t create a new
context.Background()deep in your call stack when you already have a request context. - Don’t ignore cancellation — leaking goroutines is a common source of production issues.
8. Context in gRPC and Microservices
In gRPC, the context is automatically propagated. This is extremely powerful:
- Deadlines set by the client are respected by the server.
- Cancellation flows through the entire call chain.
- You can attach metadata (trace IDs, auth tokens) that travels with the request.
This is one of the reasons gRPC + Go works so well for distributed systems.
Conclusion
The context package is not optional knowledge for Go backend engineers — it is foundational.
Once you start treating context as a first-class citizen in your code (passing it everywhere, respecting cancellation, setting proper timeouts), your services become significantly more robust and easier to operate.
In the next posts we will continue exploring production-ready Go patterns, including graceful shutdown and more advanced concurrency techniques.
Happy coding!