Context
| Background vs TODO | |
| Cancel on demand | |
| Timeouts | |
| Values with care |
Background vs TODO
Context carries cancellation and deadlines down the call chain. Background starts an empty context at the top of the program. TODO marks a spot where a real context belongs later, usually while refactoring. Both flow into functions as the first argument by convention.
package main
import (
"context"
"fmt"
)
func fetch(ctx context.Context) string {
select {
case <-ctx.Done():
return "canceled"
default:
return "data"
}
}
func main() {
fmt.Println(fetch(context.Background()))
fmt.Println(fetch(context.TODO()))
}
data
data
The Done channel stays open while work may continue, so both calls return data here. Later sections close that channel on purpose. Prefer Background in main and tests, and replace each TODO before the code ships.
Cancel on demand
WithCancel returns a child context plus a cancel function. Calling cancel closes the Done channel for that context and all its children. Workers watch Done and stop early instead of finishing stale work. Always call cancel, usually with defer, to release resources.
package main
import (
"context"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
cancel()
<-ctx.Done()
fmt.Println(ctx.Err())
}
context canceled
Waiting on Done blocks until cancel runs, so this order always prints the same line. Err explains why the context ended, which helps logging. Real programs cancel from timeouts, user actions, or shutdown hooks.
Timeouts
WithTimeout adds a deadline that cancels the context automatically. Slow work loses the race and reports a deadline error. The pattern below is the standard way to bound any call. Timeouts from the client lesson pair well with this tool.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(200 * time.Millisecond):
fmt.Println("finished")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
context deadline exceeded
The work needs 200 milliseconds but the context allows only 50, so the timeout wins. defer cancel frees the timer even on the fast path. WithDeadline is the sibling for an exact clock time instead of a duration.
Values with care
Context can also carry request-scoped values like IDs and tokens. Use an unexported key type so other packages cannot collide with your keys. Values are for transit data only, never for function options. When in doubt, pass an explicit argument instead.
package main
import (
"context"
"fmt"
)
type userKey struct{}
func main() {
ctx := context.WithValue(context.Background(), userKey{}, "ada")
fmt.Println(ctx.Value(userKey{}))
}
ada
Next: Goroutines
Article author: Arthur Isaev