Code of the Day
AdvancedConcurrency

Context

Propagate cancellation and deadlines across goroutines and API boundaries with the context package.

GoAdvanced12 min read
Recommended first
By the end of this lesson you will be able to:
  • 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 . 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 yet

Background 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 resources

Calling 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. 1.
    You create a context with context.WithCancel but the operation finishes before cancel() is called. What is the consequence?
  2. 2.
    The Go convention is to pass context.Context as the first parameter of any function that performs I/O.
  3. 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.

Finished reading? Mark it complete to track your progress.

On this page