context Basics
10 examples to get you started with the context Package - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir ctxdemo && cd ctxdemo && go mod init example.com/ctxdemo. - Save each snippet as
main.go(or separate files in one package) and run withgo run ..
Basic Examples
1. context.Background and TODO
Root contexts start chains that have no parent cancellation.
package main
import (
"context"
"fmt"
)
func main() {
fmt.Println(context.Background().Err() == nil)
fmt.Println(context.TODO().Err() == nil)
}context.Background()is the top-level empty context formain, servers, and tests.context.TODO()is a placeholder during refactors when the parent is not yet wired.- Neither carries a deadline or values until you wrap them.
Related: context.Context: Cancellation as a First-Class API - why context exists
2. WithCancel
Manual cancellation stops descendant work.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
<-ctx.Done()
fmt.Println("stopped:", ctx.Err())
}()
time.Sleep(50 * time.Millisecond)
cancel()
time.Sleep(20 * time.Millisecond)
}WithCancelreturns a child context and acancelfunction.- Always
defer cancel()to release resources even if you cancel early. ctx.Err()returnscontext.Canceledafter manual cancel.
Related: Cancellation Propagation in HTTP Handlers - real request lifetimes
3. WithTimeout
A relative deadline cancels automatically.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
select {
case <-time.After(250 * time.Millisecond):
fmt.Println("finished work")
case <-ctx.Done():
fmt.Println("timed out:", ctx.Err())
}
}WithTimeout(parent, d)is shorthand forWithDeadline(parent, time.Now().Add(d)).- When the deadline passes,
Done()closes andErr()iscontext.DeadlineExceeded. - Shorter child timeouts should stay within the parent deadline budget.
Related: Deadlines & Timeouts Across Service Boundaries - service budgets
4. WithDeadline
Absolute wall-clock end times suit upstream-provided expiry.
package main
import (
"context"
"fmt"
"time"
)
func main() {
deadline := time.Now().Add(80 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
if d, ok := ctx.Deadline(); ok {
fmt.Println("deadline:", d.Format(time.RFC3339))
}
<-ctx.Done()
fmt.Println(ctx.Err())
}Deadline()reports the cutoff and whether a deadline is set.- Prefer
WithTimeoutwhen you think in durations; useWithDeadlinewhen syncing to an external timestamp. - Parent deadlines constrain children - a child cannot outlive its parent.
Related: Deadlines & Timeouts Across Service Boundaries - propagating expiry
5. Checking ctx.Done() in a loop
Long work must poll cancellation between iterations.
package main
import (
"context"
"fmt"
"time"
)
func work(ctx context.Context) error {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
time.Sleep(30 * time.Millisecond)
fmt.Println("tick", i)
}
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
fmt.Println(work(ctx))
}- A single
selectwithdefaultavoids blocking when ctx is still active. - Return
ctx.Err()so callers distinguish cancel vs other failures. - Blocking APIs should accept
ctxinstead of hand-rolled polling when available.
Related: Testing Code That Accepts context.Context - asserting cancel paths
6. Passing ctx as the first parameter
The standard function shape propagates lifecycle down the stack.
package main
import (
"context"
"fmt"
)
func fetch(ctx context.Context, id int) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
return fmt.Sprintf("user-%d", id), nil
}
func handler(ctx context.Context) error {
name, err := fetch(ctx, 42)
if err != nil {
return err
}
fmt.Println(name)
return nil
}
func main() {
_ = handler(context.Background())
}- Name the parameter
ctxand place it first - reviewers and linters expect this. - Check
ctx.Err()before expensive work when calls are deep. - Pass the same
ctxdown; do not create freshBackground()mid-request.
Related: context Package Best Practices - team naming rules
7. WithValue for request metadata
Typed keys carry cross-cutting data sparingly.
package main
import (
"context"
"fmt"
)
type ctxKey string
const requestIDKey ctxKey = "requestID"
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func requestID(ctx context.Context) string {
v, _ := ctx.Value(requestIDKey).(string)
return v
}
func main() {
ctx := withRequestID(context.Background(), "req-abc")
fmt.Println(requestID(ctx))
}- Use unexported key types to avoid collisions across packages.
- Values are for request-scoped metadata, not optional parameters.
- Prefer explicit function arguments for business data.
Related: context Values: When and When Not - value discipline
Intermediate Examples
8. Child cancel does not stop parent
Cancellation flows parent to child only.
package main
import (
"context"
"fmt"
)
func main() {
parent, parentCancel := context.WithCancel(context.Background())
defer parentCancel()
child, childCancel := context.WithCancel(parent)
defer childCancel()
childCancel()
fmt.Println("parent err:", parent.Err())
fmt.Println("child err:", child.Err())
}- Calling
childCancel()setschild.Err()tocontext.Canceled. - The parent remains active until
parentCancel()runs. - Design APIs so upstream owns the root lifecycle.
Related: Context Misuse Anti-Patterns - lifetime mistakes
9. Deriving a shorter timeout from a parent
Each layer should subtract overhead from the remaining budget.
package main
import (
"context"
"fmt"
"time"
)
func callDownstream(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, 50*time.Millisecond)
defer cancel()
select {
case <-time.After(200 * time.Millisecond):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
parent, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
fmt.Println(callDownstream(parent))
}- Child timeouts must be less than or equal to the parent's remaining time.
- Middleware often shaves milliseconds for serialization before calling services.
- Log
DeadlineExceededat the layer that set the shortest timeout for easier debugging.
Related: Deadlines & Timeouts Across Service Boundaries - hop budgets
10. context.WithCancelCause (Go 1.20+)
Attach a reason to cancellation for clearer errors.
package main
import (
"context"
"errors"
"fmt"
)
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil)
cause := errors.New("upstream validation failed")
cancel(cause)
fmt.Println(context.Cause(ctx))
}WithCancelCausepairs withcontext.Cause(ctx)for observability.- Use causes for domain-specific stop reasons, not generic logging strings.
- Still return
ctx.Err()at API boundaries unless callers need the cause.
Related: Testing Code That Accepts context.Context - asserting cancel causes
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).