Go Context — Cancellation, Deadlines, and Values with context.Context
In this tutorial, you will learn about Go Context. We cover key concepts, practical examples, and best practices to help you master this topic.
Go context.Context propagates cancellation signals, deadlines, and request-scoped values across API boundaries and Goroutine hierarchies.
What You'll Learn
- Creating contexts with Background and TODO
- Context cancellation with cancel
- Context deadlines and timeouts
- Passing request-scoped values
Why It Matters
Context enables graceful shutdown and request lifecycle management. Docker uses context for build cancellation. Kubernetes uses context for request timeouts. DodaZIP uses context for file processing cancellation.
Real-World Use
HTTP request cancellation, database query timeouts, graceful shutdown, distributed tracing, request-scoped logging.
flowchart LR
A["Context"] --> B["Background/TODO"]
A --> C["WithCancel"]
A --> D["WithDeadline"]
A --> E["WithValue"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Context with Cancel
func operation(ctx context.Context) {
select {
case <-time.After(2 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Println("Cancelled:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go operation(ctx)
time.Sleep(1 * time.Second)
cancel()
time.Sleep(100 * time.Millisecond)
}
Context with Timeout
func slowOperation(ctx context.Context) (string, error) {
ch := make(chan string, 1)
go func() {
time.Sleep(3 * time.Second)
ch <- "result"
}()
select {
case res := <-ch:
return res, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
result, err := slowOperation(ctx)
if err != nil {
fmt.Println("Error:", err) // context deadline exceeded
return
}
fmt.Println(result)
}
HTTP Server with Context
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := processRequest(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
log.Println("Request cancelled by client")
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Result: %s", result)
}
func processRequest(ctx context.Context) (string, error) {
select {
case <-time.After(2 * time.Second):
return "done", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
Context with Values
type contextKey string
const (
UserIDKey contextKey = "user_id"
TraceIDKey contextKey = "trace_id"
)
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, UserIDKey, userID)
}
func WithTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, TraceIDKey, traceID)
}
func GetUserID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(UserIDKey).(string)
return id, ok
}
func main() {
ctx := context.Background()
ctx = WithTraceID(ctx, "abc-123")
ctx = WithUserID(ctx, "user-42")
if traceID, ok := ctx.Value(TraceIDKey).(string); ok {
fmt.Println("Trace:", traceID)
}
if userID, ok := GetUserID(ctx); ok {
fmt.Println("User:", userID)
}
}
Database Query with Context
func queryDatabase(ctx context.Context) error {
db, _ := sql.Open("postgres", "postgres://...")
defer db.Close()
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
row := db.QueryRowContext(ctxTimeout, "SELECT count(*) FROM users")
var count int
if err := row.Scan(&count); err != nil {
return err
}
fmt.Println("Users:", count)
return nil
}
Common Mistakes
1. Ignoring ctx.Done()
func handler(ctx context.Context) {
// Must check ctx.Done() in long-running operations
for {
select {
case <-ctx.Done():
return
default:
// do work
}
}
}
2. Not Calling cancel
ctx, cancel := context.WithCancel(parent)
// defer cancel() // Missing! Context leaks resources
3. Using Context for Optional Params
// Bad: Use context for optional parameters
// Good: Use explicit function parameters
ctx = context.WithValue(ctx, "page", 1)
4. Passing nil Context
func doSomething(ctx context.Context) {
// Pass context.Background() or context.TODO()
// Never pass nil
}
5. Storing Context in Structs
Context should be passed as a function parameter, not stored in structs.
Practice Questions
1. What is context.Background? The root context, never cancelled. Used in main() and top-level handlers.
2. What is context.TODO? A placeholder when you haven't decided which context to use. Should be replaced with real context.
3. How does WithTimeout work? Returns a context and cancel function. Context automatically cancels after the duration.
4. Can context values be concurrent-safe? Yes, context is immutable and safe for concurrent access.
Challenge: Write an HTTP handler that uses context timeout to limit database queries to 100ms.
Solution
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
queryCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
result := make(chan string, 1)
go func() {
result <- queryDatabase()
}()
select {
case res := <-result:
fmt.Fprint(w, res)
case <-queryCtx.Done():
http.Error(w, "query timeout", http.StatusGatewayTimeout)
}
}
FAQ
{{< faq question="Should I pass context as first parameter?" >}}
Yes, convention is context as the first parameter, typically named ctx. For example: func DoSomething(ctx context.Context, arg string).
{{< /faq >}}
{{< faq question="What happens if context is cancelled while a goroutine is working?" >}} The ctx.Done() channel closes. Goroutines should select on ctx.Done() to detect cancellation and clean up. {{< /faq >}}
{{< faq question="Can I add multiple values to context?" >}}
Yes, by wrapping: ctx = context.WithValue(ctx, key1, val1) then ctx = context.WithValue(ctx, key2, val2).
{{< /faq >}}
{{< faq question="When does context deadline exceeded occur?" >}} When using WithTimeout or WithDeadline and the time expires before the operation completes. {{< /faq >}}
{{< faq question="What is the difference between Background and TODO?" >}} Background is the root context for the main function. TODO is a placeholder for code that should receive a real context. {{< /faq >}}
Try It Yourself
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
select {
case <-time.After(2 * time.Second):
fmt.Println("completed")
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
}
}
Expected output:
cancelled: context deadline exceeded
What's Next
Now that you understand context, explore worker pools for managing concurrent work.
| Topic | Description | Link |
|---|---|---|
| Go Worker Pools | Managing concurrent work | {{< ref "23-worker-pools" >}} |
| Go Concurrency Patterns | Advanced patterns | {{< ref "24-concurrency-patterns" >}} |
| Go HTTP Servers | Building web servers | {{< ref "27-http-server" >}} |