Skip to content

Context

context.Context is a standard interface that carries:

  • A cancellation signal — a goroutine can check ctx.Done() to learn that it should stop working
  • An optional deadline or timeout — work is automatically cancelled when the clock expires
  • Request-scoped key/value pairs — small amounts of metadata like a request ID or authentication token

The context package solves a concrete problem: when a server request is cancelled (the user closes the browser, the upstream times out), how does every in-flight goroutine spawned for that request know to stop? Passing context.Context as the first argument to every I/O function is the Go-standard answer.

// Root contexts — never carry cancellation
context.Background() // top-level root, use in main or test setup
context.TODO() // placeholder when you are unsure which context to use
// Derived contexts — add cancellation/timeout on top of a parent
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
ctx2 := context.WithValue(parent, key, value)

Derived contexts form a tree. Cancelling a parent automatically cancels all its children.

When you call context.WithCancel or context.WithTimeout, you receive two values: the derived context and a cancel function. You must call cancel() when the work is done — even if the context expires naturally. If you don’t, the parent context keeps a reference to the child until the parent is also cancelled, which leaks resources.

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always

Inside a goroutine, ctx.Done() returns a channel that is closed when the context is cancelled or times out. Use select to multiplex it with real work:

select {
case result := <-workCh:
return result, nil
case <-ctx.Done():
return "", ctx.Err() // context.Canceled or context.DeadlineExceeded
}

context.WithValue attaches a key/value pair to the context. Use only for request-scoped data that crosses API boundaries (request ID, auth token). Do not use it as a general-purpose parameter-passing mechanism — put those in function arguments instead.

To avoid key collisions with other packages, always use a package-private, unexported type as the key:

type contextKey string
const requestIDKey contextKey = "requestID"
package main
import (
"context"
"fmt"
)
func fetchData(ctx context.Context, id int) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
return fmt.Sprintf("data-%d", id), nil
}
}
type ctxKey string
func main() {
// WithCancel: normal fetch succeeds
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result, err := fetchData(ctx, 1)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("got:", result)
}
// Cancel, then try again — now the context is done
cancel()
result2, err2 := fetchData(ctx, 2)
if err2 != nil {
fmt.Println("cancelled:", err2)
} else {
fmt.Println("got:", result2)
}
// WithValue: attach request-scoped metadata
ctx3 := context.WithValue(context.Background(), ctxKey("requestID"), "abc-123")
rid := ctx3.Value(ctxKey("requestID")).(string)
fmt.Println("requestID:", rid)
}
What is the correct idiomatic way to handle the cancel function from context.WithCancel?
What channel does ctx.Done() return, and when is it closed?
What does ctx.Err() return when the context deadline has exceeded?
Why should you use an unexported package-private type as a context.WithValue key?