Every server, client and long-running operation in Go is expected to accept a context and stop when it's done.
A context.Context carries a cancellation signal, an optional deadline and request-scoped values down a call chain. By convention it is the first parameter, named ctx: func Fetch(ctx context.Context, url string) error.
Contexts form a tree. Start from context.Background() (in main, tests, top-level) and derive children:
ctx, cancel := context.WithCancel(parent)— cancelled when you callcancel().context.WithTimeout(parent, 2*time.Second)/WithDeadline— cancelled automatically at the deadline.- Cancelling a parent cancels all its children.
Always `defer cancel()` right after creating a derived context; it releases the timer and resources even when the work finishes early. go vet warns if you forget.
Code doing the work checks for cancellation:
<-ctx.Done()is a channel that closes on cancellation — use it in aselect.ctx.Err()then returnscontext.Canceledorcontext.DeadlineExceeded.- Pass
ctxon to everything that blocks:http.NewRequestWithContext, database queries, your own functions.
context.WithValue attaches request-scoped data such as a trace ID, using an unexported key type to avoid collisions. Don't use it to pass optional function parameters.
Don't store contexts in structs; pass them explicitly. Use context.TODO() as a placeholder when you haven't plumbed one through yet. Go 1.21 added context.WithoutCancel and context.AfterFunc for advanced cases.
Example
package main
import (
"context"
"fmt"
"time"
)
func work(ctx context.Context, steps int) error {
for i := range steps {
select {
case <-ctx.Done():
return fmt.Errorf("stopped at step %d: %w", i, ctx.Err())
case <-time.After(20 * time.Millisecond):
// one unit of work done
}
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := work(ctx, 10)
fmt.Println(err) // stopped at step 2: context deadline exceeded
}Remember
- Pass
ctx context.Contextas the first parameter, never store it - Derive with WithCancel/WithTimeout and always
defer cancel() - Blocking code selects on
ctx.Done()and returnsctx.Err()
Watch out for
- Ignoring ctx inside a loop or blocking call, so cancellation never actually stops the work.