← Files GophersARCHIVED FILE

skills/go-context/references/cancellation-and-deadlines.md

3.12 KB · Oct 3, 2026 · 06:31 UTC

↓ Download file

# Cancellation, Timeouts, and Background Work

A `Context` cancels in one of three ways: the parent cancels, the deadline expires, or someone calls the returned `cancel` function. Children inherit cancellation automatically — that is the whole point.

## `WithCancel`

For manual cancellation, typically when a caller controls a long-running goroutine.

```go
ctx, cancel := context.WithCancel(parent)
defer cancel()

go worker(ctx)
// later, somewhere else:
cancel() // worker observes <-ctx.Done()
```

If you forget `defer cancel()`, the child context is retained until the parent dies — usually a leak.

## `WithTimeout` and `WithDeadline`

`WithTimeout(parent, d)` is sugar for `WithDeadline(parent, time.Now().Add(d))`.

```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

if err := client.Do(ctx, req); err != nil {
    // err may be context.DeadlineExceeded or a wrapped form
    return fmt.Errorf("calling client: %w", err)
}
```

The downstream call is responsible for observing the deadline — usually by passing `ctx` into another `*Context` API.

## Listening for Cancellation in a Loop

If you have a worker that does its own thing without calling another `ctx`-aware API, you must poll:

```go
for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    case job := <-jobs:
        if err := handle(ctx, job); err != nil {
            return err
        }
    }
}
```

Returning `ctx.Err()` (`Canceled` or `DeadlineExceeded`) tells the caller why you stopped.

## `context.AfterFunc` (Go 1.21+)

Register a callback that runs when the context is done, without spinning up a goroutine just to wait:

```go
stop := context.AfterFunc(ctx, func() {
    conn.Close() // free the resource when the request ends
})
defer stop()
```

`stop()` removes the callback if it has not yet run.

## `context.WithoutCancel` (Go 1.21+)

For background work that must outlive the parent request — audit logs, async write-behind, fire-and-forget metrics.

```go
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := h.do(r.Context()); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    // Audit log must survive even after the response is written
    bg := context.WithoutCancel(r.Context())
    go h.audit(bg)
}
```

`WithoutCancel` preserves the parent's *values* (request ID, trace) but drops its cancellation chain. Without it, the audit goroutine would be cancelled the instant the HTTP server finished writing the response.

## Cancellation in `errgroup`

```go
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
    g.Go(func() error { return fetch(ctx, url) })
}
return g.Wait()
```

The first error cancels `ctx`; remaining goroutines observe `<-ctx.Done()` and exit. The caller gets the first error from `g.Wait()`.

## Anti-Patterns

- `time.Sleep(d)` instead of `context.WithTimeout` — does not respect parent cancellation.
- Spawning a goroutine that does not take `ctx` — it cannot be cancelled.
- Calling `cancel()` and then continuing to use `ctx` for normal work — the context is dead.
- Catching `ctx.Err()` and returning `nil` — hides cancellation from the caller.

SHA-256: 90f238a9baea941309db222cae28e2387824c18a490cad3057547f6ef0525bcc