Context and Cancellation
Overview
The context package manages deadlines, cancellation, and request-scoped values across API boundaries.
Creating Contexts
ctx := context.Background() // Root context
ctx := context.TODO() // PlaceholderContext Hierarchy & Downward Cancellation
Contexts form an immutable tree. When a parent context is canceled or times out, all child contexts derived from it are automatically canceled. However, canceling a child context does not affect the parent or siblings.
flowchart TD
Root["context.Background() (Root)"] --> ReqCtx["reqCtx (WithValue: request_id)"]
ReqCtx --> DBTimeout["dbCtx (WithTimeout: 2s)"]
ReqCtx --> RPCCancel["rpcCtx (WithCancel)"]
RPCCancel --> SubWorker1["subWorkerCtx 1"]
RPCCancel --> SubWorker2["subWorkerCtx 2"]
classDef cancelFill fill:#f9f,stroke:#333,stroke-width:2px;
class RPCCancel,SubWorker1,SubWorker2 cancelFill;
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
return
case <-time.After(time.Hour):
fmt.Println("completed")
}
}()
cancel() // Signal cancellation downwards to all derived contextsTimeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case result := <-doWork(ctx):
fmt.Println(result)
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}Deadline
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()Passing Context
func handleRequest(ctx context.Context) {
result, err := fetchData(ctx)
if err != nil {
return
}
processData(ctx, result)
}
func fetchData(ctx context.Context) (Data, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
return http.DefaultClient.Do(req)
}Context Values
type key string
const userKey key = "user"
ctx := context.WithValue(ctx, userKey, "alice")
user := ctx.Value(userKey).(string)Best Practices
- Pass context as first parameter
- Never store context in structs
- Always call cancel() (use defer)
- Only use context values for request-scoped data
Summary
| Function | Purpose |
|---|---|
WithCancel |
Manual cancellation |
WithTimeout |
Duration-based timeout |
WithDeadline |
Absolute time limit |
WithValue |
Request-scoped data |
Worked example
Context-aware worker that respects cancel and timeout.
Save as main.go. Then:
go mod init example
go run .package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, out chan<- string) {
select {
case <-time.After(30 * time.Millisecond):
out <- fmt.Sprintf("worker-%d done", id)
case <-ctx.Done():
out <- fmt.Sprintf("worker-%d %v", id, ctx.Err())
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
out := make(chan string, 2)
var wg sync.WaitGroup
for i := 1; i <= 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(ctx, id, out)
}(i)
}
wg.Wait()
close(out)
for msg := range out {
fmt.Println(msg)
}
}Expected output: (both lines show deadline exceeded)
worker-1 context deadline exceeded
worker-2 context deadline exceeded
More examples
Propagate request IDs with typed WithValue keys.
package main
import (
"context"
"fmt"
)
type key int
const requestID key = 1
func handle(ctx context.Context) {
fmt.Println("request_id:", ctx.Value(requestID))
}
func main() {
ctx := context.WithValue(context.Background(), requestID, "req-100")
// Child inherits values.
child, cancel := context.WithCancel(ctx)
defer cancel()
handle(child)
}Expected output:
request_id: req-100
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"context"
"fmt"
"time"
)
type ctxKey string
const requestIDKey ctxKey = "request_id"
func work(ctx context.Context, name string, d time.Duration) error {
select {
case <-time.After(d):
fmt.Printf("%s finished (request_id=%v)\n", name, ctx.Value(requestIDKey))
return nil
case <-ctx.Done():
fmt.Printf("%s cancelled: %v\n", name, ctx.Err())
return ctx.Err()
}
}
func main() {
// WithValue: request-scoped data flows downward
root := context.WithValue(context.Background(), requestIDKey, "req-42")
// Manual cancel
ctx, cancel := context.WithCancel(root)
go func() {
time.Sleep(20 * time.Millisecond)
cancel()
}()
_ = work(ctx, "cancel-demo", 200*time.Millisecond)
// Timeout: child inherits parent values
tctx, tcancel := context.WithTimeout(root, 15*time.Millisecond)
defer tcancel()
_ = work(tctx, "timeout-demo", 200*time.Millisecond)
// Success path: finishes before deadline
okCtx, okCancel := context.WithTimeout(root, 100*time.Millisecond)
defer okCancel()
_ = work(okCtx, "ok-demo", 5*time.Millisecond)
// Parent cancel cancels children
parent, pCancel := context.WithCancel(root)
child, childCancel := context.WithCancel(parent)
defer childCancel()
pCancel()
select {
case <-child.Done():
fmt.Println("child saw parent cancel:", child.Err())
case <-time.After(time.Second):
fmt.Println("child did not cancel (unexpected)")
}
}Expected output:
cancel-demo cancelled: context canceled
timeout-demo cancelled: context deadline exceeded
ok-demo finished (request_id=req-42)
child saw parent cancel: context canceled
What to notice: Always defer cancel() (or call it when done) so timers and resources are released. Cancellation propagates down the tree; canceling a child never cancels its parent. Prefer typed keys for WithValue, not bare strings.
Try next: Swap WithTimeout for WithDeadline(time.Now().Add(...)). Pass the cancelled context into a second goroutine and confirm both exit.