The context Package in Go
Master Go's context package — cancellation, timeouts, deadlines, values, and how to propagate them through your application stack.
What you'll learn
- ✓What context.Context is and why every Go API uses it
- ✓Cancellation propagation with WithCancel
- ✓Timeouts and deadlines with WithTimeout and WithDeadline
- ✓Best practices for context usage
Prerequisites
- •Go basics (goroutines, channels)
- •Understanding of concurrent request handling
The context package solves a fundamental problem: how do you tell a tree of goroutines to stop? Every Go server handler, database call, and HTTP client accepts a context.Context as its first parameter. Here is how to use it.
The Context interface
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Done()returns a channel that closes when the context is cancelled.Err()returns why:context.Canceledorcontext.DeadlineExceeded.Deadline()returns the time when the context will auto-cancel.Value()carries request-scoped data (use sparingly).
Creating contexts
Background and TODO
ctx := context.Background() // root context, never cancelled
ctx := context.TODO() // placeholder when you haven't decided yet
Use Background() in main(), initialization, and tests. Use TODO() when a function needs a context but you haven’t wired it through yet.
WithCancel
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case <-ctx.Done():
fmt.Println("Worker stopped:", ctx.Err())
return
case <-time.After(5 * time.Second):
fmt.Println("Work done")
}
}()
time.Sleep(2 * time.Second)
cancel() // signals all goroutines listening on ctx.Done()
WithTimeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// could be context.DeadlineExceeded
fmt.Println("Request failed:", err)
return
}
defer resp.Body.Close()
WithDeadline
Like WithTimeout but you specify an absolute time.
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
Propagation through a call stack
Context flows down. Every function that might block should accept ctx context.Context as its first parameter.
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := fetchUser(ctx, r.URL.Query().Get("id"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(user)
}
func fetchUser(ctx context.Context, id string) (*User, error) {
row := db.QueryRowContext(ctx, "SELECT name, email FROM users WHERE id = $1", id)
var u User
if err := row.Scan(&u.Name, &u.Email); err != nil {
return nil, fmt.Errorf("querying user: %w", err)
}
return &u, nil
}
If the client disconnects, r.Context() cancels. The database query stops. No wasted work.
Context values
Carry request-scoped data like request IDs, auth tokens, or trace spans.
type contextKey string
const requestIDKey contextKey = "requestID"
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func getRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return "unknown"
}
Use unexported key types to avoid collisions across packages.
Listening for cancellation in goroutines
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker shutting down")
return
case job := <-jobs:
process(job)
}
}
}
Best practices
- Always pass context as the first parameter. Convention:
func Foo(ctx context.Context, ...). - Always call cancel. Use
defer cancel()immediately after creating a derived context. - Do not store contexts in structs. Pass them explicitly through function calls.
- Use values sparingly. Only for request-scoped data that crosses API boundaries (trace IDs, auth tokens). Never for function parameters.
- Check ctx.Err() before starting expensive work.
func expensiveOperation(ctx context.Context) error {
if ctx.Err() != nil {
return ctx.Err()
}
// proceed with expensive work...
return nil
}
Summary
The context package is Go’s standard mechanism for cancellation, timeouts, and request-scoped values. Pass it as the first parameter, always defer the cancel function, and check ctx.Done() in long-running goroutines. It is the connective tissue that lets your entire call stack respond to shutdown signals.
Related articles
- Go Goroutines and Channels in Go: Concurrency Basics
Learn Go concurrency from the ground up: launching goroutines, communicating via channels, using select, range, and building a worker pool pattern.
- Go Go Concurrency: Fan-Out, Pipeline, and Worker Pool
Master advanced Go concurrency patterns including fan-out/fan-in, pipelines, and worker pools with practical examples and production-ready code.
- Go Go Channel and Select Statement Patterns
Master Go channels and select statements with patterns for fan-out, fan-in, timeouts, cancellation, and pipeline composition.
- Go Race Detector and Debugging Concurrent Go Code
Use Go's race detector to find and fix data races, plus tools and patterns for debugging concurrent programs effectively.