Cancellation with Context Package
The context package in Go provides a way to carry deadlines, cancellation signals, and other request-scoped values across API boundaries and between goroutines. It's particularly important for controlling the lifetime of concurrent operations.
What is Context?#
A context is an immutable structure that provides:
- Cancellation signals to goroutines
- Deadlines for completing operations
- Key-value pairs for request-scoped data
The context package was introduced to solve problems with managing concurrency, especially in network servers where operations might need to be cancelled due to client disconnections, timeouts, or user cancellations.
Why Use Context?#
Without context, it can be difficult to cleanly cancel goroutines or pass request-specific information through your program. The context package provides a standard way to handle:
- Cancellation: Stopping goroutines that are no longer needed
- Deadlines: Ensuring operations complete within a time limit
- Request values: Passing request-scoped values between functions and goroutines
The Context Interface#
The Context interface in Go is defined as:
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}}
Let's understand each method:
Deadline(): Returns the time when the context will be cancelled (if a deadline is set)Done(): Returns a channel that's closed when the context is cancelledErr(): Returns the error why the context was cancelled (if it was)Value(key): Returns the value associated with a key in the context
Creating Contexts#
The context package provides several functions to create contexts:
context.Background()#
Creates an empty, non-cancellable context. This is typically used as the root of a context tree:
ctx := context.Background()
context.TODO()#
Similar to Background(), but indicates that we're not sure which context to use yet:
ctx := context.TODO() string">"comment">// For places where you're not sure which context to use
context.WithCancel(parent)#
Creates a new context that can be cancelled:
ctx, cancel := context.WithCancel(context.Background())string">"comment">// Use ctx somewherestring">"comment">// ...string">"comment">// When done or when you need to cancelcancel()
context.WithTimeout(parent, timeout)#
Creates a context that will be cancelled after a specified duration:
string">"comment">// Create a context that will cancel after 100msctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)defer cancel() string">"comment">// Always call cancel to release resources
context.WithDeadline(parent, time)#
Creates a context that will be cancelled at a specific time:
string">"comment">// Create a context that will cancel at a specific timedeadline := time.Now().Add(5 * time.Second)ctx, cancel := context.WithDeadline(context.Background(), deadline)defer cancel() string">"comment">// Always call cancel to release resources
context.WithValue(parent, key, value)#
Creates a context with a key-value pair:
ctx := context.WithValue(context.Background(), string">"requestID", string">"123")string">"comment">// Later, retrieve the valuerequestID := ctx.Value(string">"requestID").(string)
Basic Cancellation Example#
Let's create a simple example showing how to cancel a goroutine:
package mainimport (string">"context"string">"fmt"string">"time")func doWork(ctx context.Context) {for {select {case <-ctx.Done():string">"comment">// Context was cancelledfmt.Println(string">"Work cancelled:", ctx.Err())returndefault:string">"comment">// Do some workfmt.Println(string">"Working...")time.Sleep(500 * time.Millisecond)}}}func main() {string">"comment">// Create a context with cancellationctx, cancel := context.WithCancel(context.Background())string">"comment">// Start a goroutine with the contextgo doWork(ctx)string">"comment">// Let it work for 2 secondstime.Sleep(2 * time.Second)string">"comment">// Cancel the contextcancel()string">"comment">// Give the goroutine time to process the cancellationtime.Sleep(500 * time.Millisecond)fmt.Println(string">"Main: exiting")}
When you run this program, you'll see:
Working...
Working...
Working...
Working...
Work cancelled: context canceled
Main: exiting
Cancellation with Timeout#
Here's an example using a timeout:
package mainimport (string">"context"string">"fmt"string">"time")func slowOperation(ctx context.Context) {select {case <-time.After(5 * time.Second):string">"comment">// This would complete if given enough timefmt.Println(string">"Slow operation completed")case <-ctx.Done():string">"comment">// But we might be cancelledfmt.Println(string">"Slow operation cancelled:", ctx.Err())}}func main() {string">"comment">// Create a context with a 2 second timeoutctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel() string">"comment">// Always call cancel to release resourcesfmt.Println(string">"Starting slow operation...")slowOperation(ctx)fmt.Println(string">"Main: exiting")}
When you run this program, you'll see:
Starting slow operation...
Slow operation cancelled: context deadline exceeded
Main: exiting
Context Propagation#
One of the most important aspects of contexts is that they're designed to be propagated through your program. This allows cancellation signals to flow across API boundaries and between goroutines:
package mainimport (string">"context"string">"fmt"string">"time")string">"comment">// worker is the leaf function that actually does workfunc worker(ctx context.Context, id int) {for {select {case <-ctx.Done():fmt.Printf(string">"Worker %d: stopping due to %v\n", id, ctx.Err())returndefault:fmt.Printf(string">"Worker %d: working\n", id)time.Sleep(500 * time.Millisecond)}}}string">"comment">// manager starts and manages workersfunc manager(ctx context.Context) {string">"comment">// Create a child contextctx, cancel := context.WithCancel(ctx)defer cancel() string">"comment">// Ensure all workers are cancelled when we returnstring">"comment">// Start some workersfor i := 1; i <= 3; i++ {go worker(ctx, i)}string">"comment">// Let workers run for a bitselect {case <-ctx.Done():string">"comment">// If parent context is cancelled, we'll exit herefmt.Println(string">"Manager: stopping due to", ctx.Err())case <-time.After(3 * time.Second):string">"comment">// Otherwise cancel after 3 secondsfmt.Println(string">"Manager: timeout reached, cancelling workers")}}func main() {string">"comment">// Create a root context with cancellationctx, cancel := context.WithCancel(context.Background())string">"comment">// Start a goroutine for the managergo manager(ctx)string">"comment">// Let it run for 2 seconds, then cancel everythingtime.Sleep(2 * time.Second)cancel()string">"comment">// Give things time to clean uptime.Sleep(1 * time.Second)fmt.Println(string">"Main: exiting")}
This example shows how cancellation flows from parent to child contexts. Cancelling the parent causes all children to be cancelled as well.
Using Context Values#
Context values are useful for carrying request-scoped data:
package mainimport (string">"context"string">"fmt")string">"comment">// Define key types to avoid collisiontype key stringconst (requestIDKey key = string">"requestID"userIDKey key = string">"userID")string">"comment">// Process a request with user ID and request IDfunc processRequest(ctx context.Context) {string">"comment">// Extract values from contextrequestID, ok := ctx.Value(requestIDKey).(string)if !ok {requestID = string">"unknown"}userID, ok := ctx.Value(userIDKey).(string)if !ok {userID = string">"anonymous"}fmt.Printf(string">"Processing request %s for user %s\n", requestID, userID)string">"comment">// Pass context to another functionvalidateUser(ctx)}func validateUser(ctx context.Context) {string">"comment">// Extract user ID from contextuserID, ok := ctx.Value(userIDKey).(string)if !ok {userID = string">"anonymous"}fmt.Printf(string">"Validating user: %s\n", userID)}func main() {string">"comment">// Create context with request and user IDsctx := context.Background()ctx = context.WithValue(ctx, requestIDKey, string">"REQ-123")ctx = context.WithValue(ctx, userIDKey, string">"USER-456")string">"comment">// Process the requestprocessRequest(ctx)}
When you run this program, you'll see:
Processing request REQ-123 for user USER-456
Validating user: USER-456
Best Practices for Using Context#
Here are some best practices for using the context package:
1. Always Pass Context as the First Parameter#
By convention, context should be the first parameter in functions that use it:
string">"comment">// Goodfunc ProcessRequest(ctx context.Context, req *Request) (*Response, error)string">"comment">// Not as goodfunc ProcessRequest(req *Request, ctx context.Context) (*Response, error)
2. Don't Store Context in Structs#
Context should be passed explicitly, not stored in structs:
string">"comment">// Goodtype Service struct {string">"comment">// ...}func (s *Service) Process(ctx context.Context) {string">"comment">// ...}string">"comment">// Badtype Service struct {ctx context.Contextstring">"comment">// ...}
3. Use Context Values Sparingly#
Context values should only be used for request-scoped data that crosses API boundaries, not for passing optional parameters:
string">"comment">// Good: Pass request ID in contextctx = context.WithValue(ctx, string">"requestID", requestID)string">"comment">// Bad: Pass configuration in contextctx = context.WithValue(ctx, string">"timeout", timeout)
4. Always Call Cancel Functions#
When using WithCancel, WithTimeout, or WithDeadline, always call the returned cancel function, typically with defer:
ctx, cancel := context.WithTimeout(context.Background(), timeout)defer cancel() string">"comment">// Always call cancel, even if the operation completes
5. Don't Pass nil Context#
If you're not sure which context to use, use context.TODO() instead of nil:
string">"comment">// Goodfunc doSomething() {ctx := context.TODO()string">"comment">// ...}string">"comment">// Badfunc doSomething() {var ctx context.Context string">"comment">// nil contextstring">"comment">// ...}
6. Only Cancel a Context Once#
Calling cancel() multiple times is safe, but it's best practice to ensure it's called exactly once:
string">"comment">// Goodfunc processRequest(ctx context.Context) {childCtx, cancel := context.WithCancel(ctx)defer cancel() string">"comment">// Ensures we only cancel oncestring">"comment">// ...}
Real-World Example: HTTP Server#
Here's a real-world example of using context in an HTTP server:
package mainimport (string">"context"string">"fmt"string">"log"string">"net/http"string">"time")string">"comment">// ProcessRequest simulates a long-running processfunc ProcessRequest(ctx context.Context) (string, error) {string">"comment">// Create a channel to signal completiondone := make(chan struct{})string">"comment">// Result and errorvar result stringvar err errorstring">"comment">// Start processing in a goroutinego func() {string">"comment">// Simulate worktime.Sleep(500 * time.Millisecond)string">"comment">// Check if context was cancelled before completingselect {case <-ctx.Done():string">"comment">// Do nothing, we'll handle this outsidedefault:result = string">"Processing complete!"}string">"comment">// Signal completionclose(done)}()string">"comment">// Wait for completion or cancellationselect {case <-done:string">"comment">// Processing completedreturn result, errcase <-ctx.Done():string">"comment">// Context was cancelledreturn string">"", ctx.Err()}}func handleRequest(w http.ResponseWriter, r *http.Request) {string">"comment">// Create a timeout context for this requestctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)defer cancel()string">"comment">// Process the requestresult, err := ProcessRequest(ctx)if err != nil {http.Error(w, fmt.Sprintf(string">"Processing failed: %v", err), http.StatusInternalServerError)return}string">"comment">// Return the resultfmt.Fprintln(w, result)}func main() {string">"comment">// Register handlerhttp.HandleFunc(string">"/process", handleRequest)string">"comment">// Start serverlog.Println(string">"Starting server on :8080")log.Fatal(http.ListenAndServe(string">":8080", nil))}
In this example:
- Each HTTP request has a context that's automatically cancelled if the client disconnects
- We add a timeout to the context, ensuring the operation takes no more than 1 second
- The timeout is propagated to the
ProcessRequestfunction, which honors the cancellation
Cancelling Multiple Goroutines#
Context is great for cancelling multiple goroutines at once:
package mainimport (string">"context"string">"fmt"string">"sync"string">"time")func worker(ctx context.Context, id int, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf(string">"Worker %d: starting\n", id)for {select {case <-ctx.Done():fmt.Printf(string">"Worker %d: stopping due to %v\n", id, ctx.Err())returncase <-time.After(time.Second):fmt.Printf(string">"Worker %d: doing work\n", id)}}}func main() {string">"comment">// Create a context with cancellationctx, cancel := context.WithCancel(context.Background())string">"comment">// Start a group of workersvar wg sync.WaitGroupfor i := 1; i <= 5; i++ {wg.Add(1)go worker(ctx, i, &wg)}string">"comment">// Let workers run for 3 secondstime.Sleep(3 * time.Second)string">"comment">// Cancel all workersfmt.Println(string">"Main: cancelling workers")cancel()string">"comment">// Wait for all workers to finishwg.Wait()fmt.Println(string">"Main: all workers done")}
This example shows how a single cancel() call can stop multiple goroutines at once.
Context in API Design#
When designing APIs, you should consider how context flows through your system:
package mainimport (string">"context"string">"fmt"string">"time")string">"comment">// Database represents a database connectiontype Database struct {string">"comment">// ...}string">"comment">// QueryContext executes a query with a contextfunc (db *Database) QueryContext(ctx context.Context, query string) ([]string, error) {string">"comment">// Check if context is already cancelledif ctx.Err() != nil {return nil, ctx.Err()}string">"comment">// Create a result channelresultCh := make(chan []string)errCh := make(chan error)string">"comment">// Execute query in a goroutinego func() {string">"comment">// Simulate a database querytime.Sleep(200 * time.Millisecond)string">"comment">// Return resultsresultCh <- []string{string">"result1", string">"result2"}}()string">"comment">// Wait for result or cancellationselect {case result := <-resultCh:return result, nilcase err := <-errCh:return nil, errcase <-ctx.Done():string">"comment">// Ideally we would also cancel the database operation herereturn nil, ctx.Err()}}string">"comment">// Service uses the databasetype Service struct {DB *Database}string">"comment">// ProcessRequest handles a requestfunc (s *Service) ProcessRequest(ctx context.Context, request string) ([]string, error) {string">"comment">// We might add a timeout for this specific operationctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)defer cancel()string">"comment">// Execute queryreturn s.DB.QueryContext(ctx, string">"SELECT * FROM "+request)}func main() {string">"comment">// Setupdb := &Database{}service := &Service{DB: db}string">"comment">// Create a context with cancellationctx, cancel := context.WithCancel(context.Background())defer cancel()string">"comment">// Process a requestresult, err := service.ProcessRequest(ctx, string">"users")if err != nil {fmt.Printf(string">"Error: %v\n", err)return}fmt.Printf(string">"Results: %v\n", result)}
This example shows:
- How context flows from client code to database operations
- How timeouts can be added at different levels
- How cancellation propagates through the layers
Summary#
The context package is a powerful tool for managing goroutine lifecycles in Go. It provides a standard way to:
- Cancel operations that are no longer needed
- Set deadlines and timeouts for operations
- Propagate cancellation through call chains
- Carry request-scoped values between functions and goroutines
By following best practices with the context package, you can build more robust and responsive concurrent programs that properly handle cancellation and timeouts.
Remember:
- Always pass context as the first parameter
- Don't store context in structs
- Use context values sparingly
- Always call cancel functions with defer
- Don't pass nil contexts
- Cancel a context only once
- Check for cancellation regularly in long-running operations
With these guidelines, you'll be able to write Go applications that gracefully handle cancellation signals and deadlines in complex concurrent scenarios.