Context Complete
Context Complete
Overview
context.Context carries cancellation, deadlines, and optional request-scoped values. Prefer it over ad-hoc cancel channels for public APIs.
From cancel channel to context
// Channel style
func execute(cancel <-chan struct{}, fn func() int) (int, error) {
ch := make(chan int, 1)
go func() { ch <- fn() }()
select {
case res := <-ch:
return res, nil
case <-cancel:
return 0, errors.New("canceled")
}
}
// Context style
func execute(ctx context.Context, fn func() int) (int, error) {
ch := make(chan int, 1)
go func() { ch <- fn() }()
select {
case res := <-ch:
return res, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}ctx, cancel := context.WithCancel(context.Background())
defer cancel() // alwaysMultiple cancel() calls are safe (unlike double close on a channel).
Context is a tree
Background
└── WithCancel / WithTimeout
└── children inherit cancel downward
shorter deadline always wins
| Rule | Detail |
|---|---|
| Immutable nodes | “Add” property → new child |
| Parent cancel | cancels all children |
| Child cancel | does not cancel parent/siblings |
| Timeouts nest | shorter deadline wins; child cannot extend parent |
parent, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
child, cancel2 := context.WithTimeout(parent, 50*time.Millisecond)
defer cancel2()
// work with child: max wait is 50msSoft child timeout longer than parent is pointless—parent fires first.
Timeout and deadline
ctx, cancel := context.WithTimeout(parent, 150*time.Millisecond)
// equivalent core:
// context.WithDeadline(parent, time.Now().Add(150*time.Millisecond))
defer cancel()
// errors
// context.Canceled
// context.DeadlineExceededexecute need not know why Done closed—only that it should stop waiting.
Values
type key int
const requestID key = 1
ctx = context.WithValue(ctx, requestID, "abc")
id, _ := ctx.Value(requestID).(string)Use for request metadata (IDs, loggers)—not for optional parameters that belong in function args.
Propagate into work
Best practice: fn accepts ctx and checks ctx.Done() or passes ctx to I/O:
func work(ctx context.Context) (int, error) {
select {
case <-time.After(100 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}Otherwise timeout only abandons the waiter, not the worker.
Runnable example
go mod init example && go run .package main
import (
"context"
"fmt"
"time"
)
func execute(ctx context.Context, fn func() int) (int, error) {
ch := make(chan int, 1)
go func() { ch <- fn() }()
select {
case v := <-ch:
return v, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
v, err := execute(ctx, func() int {
time.Sleep(100 * time.Millisecond)
return 42
})
fmt.Println(v, err)
}Expected: 0 context deadline exceeded
Try next: Nest a 10ms child under a 1s parent and confirm the child wins.