Learn / Go Service Engineering
Context and Cancellation
Make timeouts and shutdown signals flow through every expensive operation.
Course: Go Service Engineering. Level: Intermediate. Topic: Reliable services.
Stage: basic - Basic lifecycle control - Context propagation and cancellation. Make request deadlines and shutdown signals flow through every expensive operation.
Outcomes
- Pass context intentionally.
- Set operation-level deadlines.
- Stop work cleanly during deploys.
- Pass context.Context as the first parameter to request-scoped work.
- Use cancellation and deadlines to stop unnecessary work.
- Avoid misusing context as optional parameter storage.
- Inject dependencies explicitly through constructors or structs.
- Implement graceful shutdown for HTTP services.
- Use retries, logging, and configuration carefully.
Concepts
- context
- deadline
- goroutine
- graceful shutdown
- Context
- Context carries cancellation
- Deadlines bound work
- Go Programming foundations
- Guided practice
- Production Patterns
- Explicit dependencies
- Graceful shutdown
- Retries need budgets
Concept flow
Show how context propagation and cancellation moves from trigger to implementation outcome in Go Services.
- HTTP request
- Service method
- Database call
- Worker
- Shutdown signal
Session flow
- Model context (concept, 9 min) — Name the decisions behind context before writing code.
- Pass context intentionally.
- Explain where context belongs in ticket processing platform.
- Build the vertical slice (walkthrough, 15 min) — Implement the smallest useful slice in jobs/worker.go.
- Set operation-level deadlines.
- Connect deadline to a working example.
- Verify and harden (exercise, 10 min) — Log cancellation separately from failure.
- Stop work cleanly during deploys.
- Record one risk or follow-up before moving on.
- Cancellation, Deadlines, and Values: Context carries cancellation (concept, 35 min) — A context tells work when its caller no longer cares about the result. HTTP requests, database calls, RPC calls, and goroutines should observe cancellation so services can shed abandoned work promptly.
- Retained source example: Select on cancellation
func WaitForResult(ctx context.Context, results <-chan string) (string, error) {
select {
case result := <-results:
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
ctx.Done closes when the context is canceled or its deadline expires.
- Cancellation, Deadlines, and Values: Deadlines bound work (walkthrough, 35 min) — Timeouts and deadlines prevent one dependency from consuming all request time. Use defer cancel() when creating a child context so timers and resources are released promptly.
- Retained source example: Timeout context
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
if err := client.DoWork(ctx); err != nil {
return fmt.Errorf("do work: %w", err)
}
Always call cancel for contexts created with WithCancel, WithTimeout, or WithDeadline.
- Cancellation, Deadlines, and Values: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Accept context.Context in functions that do request-scoped I/O or long-running work.
- Practice: Call cancel for derived contexts.
- Practice: Use typed unexported keys for context values when values are necessary.
- Avoid: Storing context in a struct instead of passing it per operation.
- Avoid: Ignoring ctx.Done in goroutines started for a request.
- Avoid: Using context values as a replacement for explicit parameters.
- Cancellation, Deadlines, and Values: references (review, 2 min) — Original references retained from the legacy library.
- Package context: https://pkg.go.dev/context
- Go Blog: Context: https://go.dev/blog/context
- Service Patterns: Explicit dependencies (concept, 30 min) — Go services often use plain structs with dependencies as fields. Constructors validate configuration and assemble dependencies. This keeps wiring visible and tests straightforward without requiring a framework.
- Retained source example: Handler with dependencies
type UserHandler struct {
users UserService
logger *slog.Logger
}
func NewUserHandler(users UserService, logger *slog.Logger) *UserHandler {
return &UserHandler{users: users, logger: logger}
}
The constructor makes dependencies explicit and easy to replace in tests.
- Service Patterns: Graceful shutdown (walkthrough, 30 min) — A service should stop accepting new work, give in-flight requests a bounded time to finish, and then release resources. Signal handling plus http.Server.Shutdown is the standard starting point.
- Retained source example: Shutdown shape
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Printf("server error: %v", err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
Shutdown uses a fresh bounded context so cleanup can complete after the signal.
- Service Patterns: Retries need budgets (walkthrough, 30 min) — Retries can amplify outages if every client retries aggressively. Use context deadlines, bounded attempts, backoff, jitter, and idempotency rules. Some operations are unsafe to retry without a request key.
- Retained source example: Retry loop outline
for attempt := 1; attempt <= maxAttempts; attempt++ {
err := call(ctx)
if err == nil {
return nil
}
if !retryable(err) || ctx.Err() != nil {
return err
}
time.Sleep(backoff(attempt))
}
Production retry code should also respect jitter and context-aware sleeps.
- Service Patterns: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Make dependencies explicit in constructors or struct fields.
- Practice: Use structured logging for request and operational events.
- Practice: Make shutdown and retry behavior context-aware.
- Avoid: Using package globals for dependencies that tests need to replace.
- Avoid: Retrying non-idempotent operations without safeguards.
- Avoid: Calling os.Exit deep in library code instead of returning errors.
- Service Patterns: references (review, 2 min) — Original references retained from the legacy library.
- Package log/slog: https://pkg.go.dev/log/slog
- Package os/signal: https://pkg.go.dev/os/signal
Code example
Go in jobs/worker.go.
package jobs
import (
"context"
"log/slog"
"time"
)
type Job struct {
ID string
Payload []byte
}
func Run(ctx context.Context, jobs <-chan Job, handle func(context.Context, Job) error) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
jobCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
if err := handle(jobCtx, job); err != nil {
slog.Error("job failed", "id", job.ID, "error", err)
}
cancel()
}
}
}
Walkthrough examples
- Context and Cancellation in a ticket processing platform — A team is extending a small Go service plus worker and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: jobs/worker.go
- File: tests/context-and-cancellation.spec
- File: docs/go-services/context-and-cancellation.md
- Start from the provided Go snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Thread context through calls" before adding extra behavior.
- Write down how the implementation changes when deadline fails or becomes slow.
- Select on cancellation — ctx.Done closes when the context is canceled or its deadline expires.
- Retained source code:
func WaitForResult(ctx context.Context, results <-chan string) (string, error) {
select {
case result := <-results:
return result, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Timeout context — Always call cancel for contexts created with WithCancel, WithTimeout, or WithDeadline.
- Retained source code:
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
if err := client.DoWork(ctx); err != nil {
return fmt.Errorf("do work: %w", err)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Request context in a handler — Request cancellation flows from net/http into downstream calls through r.Context().
- Retained source code:
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, err := h.store.FindUser(r.Context(), r.PathValue("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(user)
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Handler with dependencies — The constructor makes dependencies explicit and easy to replace in tests.
- Retained source code:
type UserHandler struct {
users UserService
logger *slog.Logger
}
func NewUserHandler(users UserService, logger *slog.Logger) *UserHandler {
return &UserHandler{users: users, logger: logger}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Shutdown shape — Shutdown uses a fresh bounded context so cleanup can complete after the signal.
- Retained source code:
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Printf("server error: %v", err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Retry loop outline — Production retry code should also respect jitter and context-aware sleeps.
- Retained source code:
for attempt := 1; attempt <= maxAttempts; attempt++ {
err := call(ctx)
if err == nil {
return nil
}
if !retryable(err) || ctx.Err() != nil {
return err
}
time.Sleep(backoff(attempt))
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Structured logging — Structured fields make logs easier to query than formatted strings.
- Retained source code:
logger.Info("request completed",
"method", r.Method,
"path", r.URL.Path,
"status", status,
"duration", time.Since(start),
)
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Add a timeout to one handler path.
- Close the jobs channel in a worker test and assert the loop exits.
- Log cancellation separately from failure.
- Remember cancel: After context.WithTimeout returns ctx and cancel, what should you usually defer?
- Expected output: cancel()
- Hint: It releases timer resources even when the timeout does not fire.
- Reference solution: Use defer cancel() soon after creating the timeout context.
- Accepted answers: cancel() | defer cancel() | cancel
- Graceful shutdown method: Which http.Server method gracefully shuts down a server with a context?
- Expected output: Shutdown
- Hint: It is different from Close because it allows in-flight requests to finish.
- Reference solution: Use http.Server.Shutdown(ctx).
- Accepted answers: Shutdown | server.Shutdown | http.Server.Shutdown
Checklist
- Thread context through calls
- Add deadline
- Handle shutdown
- Verify logs
Quiz prompts
- What should happen when a request context is canceled? — Context cancellation is the signal to release resources and stop work tied to a request or shutdown.
- A teammate wants to hide context inside a convenient helper. What should you check first? — Place context at the boundary that keeps ticket processing platform behavior explicit, testable, and reviewable.
- Which artifact best proves this Go Services lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Basic lifecycle control: a teammate says the happy path works, but "Context threading" is still implicit. What should you ask for before merging? — Context threading belongs in the basic stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this basic Go Services slice. Which evidence is strongest? — Add lifecycle control to a Go service path: propagated context, dependency timeout, worker cancellation proof, and shutdown log evidence.
- Where should context.Context usually appear in a function signature? — The convention is func Do(ctx context.Context, arg T) error.
- What should not be stored in context? — Context values are for request-scoped data crossing process or API boundaries, not general options.
- Why should retries have a budget? — Retries consume time and load; they need limits and cancellation awareness.
- What is a common Go dependency injection style? — Explicit wiring is idiomatic and testable.
Flashcards
- Basic lifecycle control: what decision does "Context threading" force you to make? Pass context through service, database, worker, and external API calls without hiding ownership. Evidence prompt: Trace one request context from handler to database call and remove any background context misuse.
- Basic lifecycle control: what decision does "Operation deadlines" force you to make? Set local deadlines for slow dependencies so one request cannot exhaust service resources. Evidence prompt: Add an operation-level timeout and test the timeout branch.
- Basic lifecycle control: what decision does "Shutdown proof" force you to make? Prove workers and in-flight requests stop gracefully during deployment. Evidence prompt: Write a test or manual proof that a worker exits when its context is canceled.
- In Go Services, what should you remember about context? context matters here because it supports "Pass context intentionally.".
- In Go Services, what should you remember about deadline? deadline matters here because it supports "Set operation-level deadlines.".
- In Go Services, what should you remember about goroutine? goroutine matters here because it supports "Stop work cleanly during deploys.".
- In Go Services, what should you remember about graceful shutdown? graceful shutdown matters here because it supports "Pass context intentionally.".
Labs
- Ship a context and cancellation slice — Extend a small Go service plus worker with a small but reviewable feature that proves the lesson's architecture in code.
- Add lifecycle control to a Go service path: propagated context, dependency timeout, worker cancellation proof, and shutdown log evidence.
- Trace one request context from handler to database call and remove any background context misuse.
- Add an operation-level timeout and test the timeout branch.
- Write a test or manual proof that a worker exits when its context is canceled.
- Add a timeout to one handler path.
- Close the jobs channel in a worker test and assert the loop exits.
- The lab demonstrates the basic lifecycle control outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Context threading, Operation deadlines, Shutdown proof.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready context and cancellation (Stretch) — Add lifecycle control to a Go service path: propagated context, dependency timeout, worker cancellation proof, and shutdown log evidence.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from jobs/worker.go plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL