Context
Propagate cancellation and deadlines across goroutines and API boundaries with the context package.
- Create a root context with context.Background() or context.TODO()
- Add cancellation with WithCancel and signal it by calling cancel()
- Attach a deadline or timeout with WithDeadline and WithTimeout
- Store and retrieve request-scoped values with WithValue
- Thread a context through a chain of function calls
Every real server eventually asks: "what should happen if the client disconnects mid-request?" or "what if this database call is taking too long?" The answer in Go is context. The context package provides a standard way to carry cancellation signals and deadlines through a chain of function calls — across goroutines, across API boundaries, and all the way down to I/O calls.
context.Background() and context.TODO()
Every context chain starts at a root:
ctx := context.Background() // the root for main(), server setup, tests
ctx := context.TODO() // placeholder when you haven't decided yetBackground is not nil and never cancels. You derive all other contexts from it (or from another derived context).
TODO signals intent to future readers — this will become a real context later. Do not leave TODO in production code for long.
WithCancel
WithCancel returns a derived context and a cancel function:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always call cancel to release resourcesCalling cancel() closes ctx.Done(), a channel that any goroutine can wait on:
go func() {
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
return
case result := <-work:
fmt.Println("done:", result)
}
}()ctx.Err() returns context.Canceled when cancelled or context.DeadlineExceeded when a deadline passes.
Always call the cancel function — even if the operation completes successfully. Failing to call cancel leaks the goroutine and resources associated with the context. The idiomatic pattern is ctx, cancel := ...; defer cancel().
WithTimeout and WithDeadline
WithTimeout is the most common pattern in production HTTP and RPC code:
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
result, err := db.QueryContext(ctx, "SELECT ...")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
// query timed out
}
// ...
}WithDeadline is the same but takes an absolute time.Time:
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(parentCtx, deadline)
defer cancel()When the timeout expires, ctx.Done() is automatically closed and any blocking call that accepts a context (database queries, HTTP clients, gRPC calls) returns with context.DeadlineExceeded.
Context in a call chain
The Go convention is to pass context as the first parameter of any function that performs I/O or may be long-running:
func (s *Server) handleRequest(ctx context.Context, req *Request) (*Response, error) {
user, err := s.db.GetUser(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("handleRequest: %w", err)
}
// ctx is passed down — if the request is cancelled, db.GetUser returns early
return buildResponse(ctx, user)
}This threading pattern means a single cancel() at the top level propagates all the way down. Libraries like database/sql, net/http, and google.golang.org/grpc all accept a context.Context as the first argument for exactly this reason.
WithValue — request-scoped data
WithValue attaches a key-value pair to a context:
type ctxKey string
const requestIDKey ctxKey = "requestID"
ctx = context.WithValue(ctx, requestIDKey, "abc-123")
// later, anywhere that has ctx:
id, ok := ctx.Value(requestIDKey).(string)Use a custom unexported type as the key (like ctxKey above) to avoid collisions with keys from other packages. If two packages both use the string "requestID" as a key, they overwrite each other. A private type is package-specific by definition.
Use WithValue sparingly — only for request-scoped metadata (request IDs, authentication tokens, trace IDs). Never use it to pass function parameters that should be explicit arguments. The rule of thumb: if removing the value would change the correctness of the logic, it should be a parameter.
Check your understanding
Knowledge check
- 1.You create a context with context.WithCancel but the operation finishes before cancel() is called. What is the consequence?
- 2.The Go convention is to pass context.Context as the first parameter of any function that performs I/O.
- 3.Why should you use a custom unexported type as a context key instead of a plain string?
Do it yourself
Write an HTTP-like handler function that accepts a context.Context. Use context.WithTimeout at the call site with a 2-second limit. Inside the handler, simulate work with a channel and a select on ctx.Done().
go run main.go
go vet ./...Where to go next
The lab brings together goroutines, channels, select, sync primitives, and context through scenario questions focused on real concurrency bugs and patterns.