context

Guided tour · Time & Context · pkg.go.dev →

Cancellation, deadlines, and request-scoped values that propagate across API boundaries and goroutines.

Carry deadlines, cancellation, and request-scoped values across goroutines and API boundaries. Pass ctx as the first argument; never store it in a struct.

Root
ctx := context.Background()
Cancel manually
ctx, cancel := context.WithCancel(parent)
defer cancel()
Time-bound
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
Check cancellation
select {
case <-ctx.Done():
    return ctx.Err()
default:
}
Carry a value (sparingly)
ctx = context.WithValue(parent, traceIDKey{}, id)

The four constructors

Every request starts from Background() or TODO(). You then wrap it to add cancellation, deadlines, timeouts, or values.

Background vs TODO

Background is the root for normal code. TODO signals 'I don't know what to use here yet' — useful during refactors. They behave identically; the name documents intent.

ctx := context.Background()  // main, init, tests
// ctx := context.TODO()   // placeholder

WithCancel — caller cancels when done

ctx, cancel := context.WithCancel(context.Background())
defer cancel()          // ALWAYS defer cancel to release resources

go doWork(ctx)
time.Sleep(100 * time.Millisecond)
cancel()                // tell doWork to stop

WithTimeout and WithDeadline

Timeout is relative, Deadline is absolute. Both return a cancel that you must defer, even if the deadline fires on its own.

ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()

ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Second))
defer cancel()

WithValue — propagate request-scoped data

Use sparingly. Meant for things like request IDs, auth info, logger — NOT function arguments in disguise. Use a private key type to avoid collisions.

type reqIDKey struct{}

ctx = context.WithValue(ctx, reqIDKey{}, "req-123")
id, _ := ctx.Value(reqIDKey{}).(string)
fmt.Println(id)

Observing cancellation

select on ctx.Done()

func doWork(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case job := <-jobs:
            process(job)
        }
    }
}

ctx.Err — why did we stop?

Returns nil while alive. After Done, returns context.Canceled or context.DeadlineExceeded.

if err := ctx.Err(); err != nil {
    return fmt.Errorf("abort: %w", err)
}

Patterns

Always pass ctx as the first argument

The idiom: func Do(ctx context.Context, ...). Never store ctx inside a struct; accept it on each call.

func Fetch(ctx context.Context, url string) ([]byte, error) { ... }

HTTP request with context

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)

signal.NotifyContext — cancel on SIGINT

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

if err := server.Shutdown(ctx); err != nil { ... }