Learn Go← Dashboard

Cancellation, deadlines, and request-scoped values.

Cancellation & Deadlines with context

The context package is how Go programs propagate cancellation, deadlines, and request-scoped values across goroutines and API boundaries. Every standard library function that does I/O over the network takes a context.Context as its first argument — and you should too.

The two roots

ctx := context.Background()        // top of a long-running process
ctx := context.TODO()              // placeholder while you decide what to use

Then you derive a new context with a deadline or a cancel function:

ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
ctx, cancel := context.WithDeadline(parent, t)
defer cancel()

You must always call cancel() (usually via defer) — even if the context finished naturally. That releases resources held by the context.

Cancellation

go playground
Loading...

ctx.Done() returns a channel that is closed when the context is cancelled or exceeded. Long-running goroutines should poll it via select.

Passing context everywhere

The convention is to pass ctx as the first argument, named ctx:

func FetchUser(ctx context.Context, id int) (*User, error) { ... }

Never store a context in a struct field — pass it through. Never pass nil for a context; use context.TODO() if you really don't have one.

Request-scoped values (with care)

You can also attach values to a context — typically things like a request ID or a per-request logger:

ctx = context.WithValue(parent, requestIDKey, reqID)
v := ctx.Value(requestIDKey)

Use unexported key types for context.Value

To prevent collisions between packages, define a private key type:

package mypkg

type ctxKey int
const requestIDKey ctxKey = 0

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
    s, _ := ctx.Value(requestIDKey).(string)
    return s
}

Using a bare string as the key would let other packages accidentally overwrite each other's values.

Checking why a context ended

err := ctx.Err()
// nil                       — still active
// context.Canceled          — somebody called cancel()
// context.DeadlineExceeded  — the deadline passed

Use errors.Is(err, context.DeadlineExceeded) when you want to distinguish a timeout from other failures (e.g., to decide whether to retry).

Why must you `defer cancel()` after `context.WithTimeout`, even when the timeout will fire on its own?