Go Context Package in 2026: Cancellation, Timeouts and Interview Questions

Master Go's context package for managing cancellation, deadlines, and request-scoped values. Complete guide with practical examples and interview questions.

Go context package illustration showing concurrency flow and cancellation patterns

The Go context package provides the standard mechanism for carrying deadlines, cancellation signals, and request-scoped values across API boundaries. Every production Go application uses context: HTTP handlers, database queries, gRPC calls, and background workers all depend on it for graceful shutdown and timeout management.

Interview Essential

Context is one of the most frequently asked topics in Go interviews. Interviewers expect candidates to explain context propagation, demonstrate proper cancellation handling, and avoid common pitfalls like storing context in structs.

The Context Interface and Its Four Methods

The Context interface defines four methods that every context implementation must satisfy. Understanding these methods forms the foundation for effective context usage.

context_interface.gogo
type Context interface {
    // Deadline returns the time when work should be canceled
    Deadline() (deadline time.Time, ok bool)
    
    // Done returns a channel that closes when the context is canceled
    Done() <-chan struct{}
    
    // Err returns the reason why Done was closed
    Err() error
    
    // Value returns the value associated with key, or nil
    Value(key any) any
}

Done() returns a receive-only channel. When this channel closes, any goroutine listening to it receives the signal immediately. The pattern <-ctx.Done() blocks until cancellation occurs. Err() explains why: either context.Canceled when explicitly canceled, or context.DeadlineExceeded when a timeout or deadline passed.

Creating Contexts with Background and TODO

Two functions create root contexts: context.Background() and context.TODO(). Both return non-nil, empty contexts that are never canceled.

context_creation.gogo
package main

import (
    "context"
    "log"
    "net/http"
)

func main() {
    // Background: the root context for your application
    ctx := context.Background()
    
    // Use it as parent for derived contexts
    server := &http.Server{Addr: ":8080"}
    
    // TODO: placeholder when context source is unclear
    // Static analysis tools can flag context.TODO() for review
    processLegacyData(context.TODO())
}

func processLegacyData(ctx context.Context) {
    // Context parameter enables future cancellation support
    log.Println("Processing data...")
}

Background() serves as the parent for incoming requests, main functions, and initialization code. TODO() acts as a placeholder during refactoring when the correct context source is not yet determined. Static analysis tools can flag TODO() usage for follow-up.

Cancellation with WithCancel and WithCancelCause

Manual cancellation allows parent goroutines to signal children that work should stop. This pattern appears in worker pools, background tasks, and graceful shutdown implementations.

cancellation.gogo
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int, results chan<- int) {
    for {
        select {
        case <-ctx.Done():
            // Check why cancellation occurred
            if cause := context.Cause(ctx); cause != nil {
                fmt.Printf("Worker %d stopped: %v\n", id, cause)
            }
            return
        default:
            // Simulate work
            time.Sleep(100 * time.Millisecond)
            results <- id * 10
        }
    }
}

func main() {
    // WithCancelCause provides error context
    ctx, cancel := context.WithCancelCause(context.Background())
    results := make(chan int, 10)
    
    // Start 3 workers
    for i := 1; i <= 3; i++ {
        go worker(ctx, i, results)
    }
    
    // Collect some results
    for i := 0; i < 5; i++ {
        fmt.Println("Result:", <-results)
    }
    
    // Cancel with a specific reason
    cancel(errors.New("shutdown requested by user"))
    
    // context.Cause retrieves the cancellation reason
    time.Sleep(50 * time.Millisecond)
    fmt.Println("Cause:", context.Cause(ctx))
}

WithCancelCause, introduced in Go 1.20, provides richer error information than plain WithCancel. The context.Cause() function retrieves the error passed to the cancel function, making debugging easier in complex systems.

Ready to ace your Go interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Timeouts and Deadlines for Request Handling

HTTP handlers, database calls, and external API requests should always have timeouts. The context package offers WithTimeout and WithDeadline for this purpose. Both create contexts that automatically cancel when time expires.

timeouts.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

// fetchFromAPI simulates an HTTP call
func fetchFromAPI(ctx context.Context, endpoint string) (string, error) {
    // Simulate variable response time
    responseTime := time.Duration(100+endpoint[0]%150) * time.Millisecond
    
    select {
    case <-time.After(responseTime):
        return fmt.Sprintf("Response from %s", endpoint), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithTimeout: relative duration
    ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    defer cancel() // Always call cancel to release resources
    
    result, err := fetchFromAPI(ctx, "api.example.com/users")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("Request timed out")
        } else {
            fmt.Println("Request canceled")
        }
        return
    }
    fmt.Println(result)
    
    // WithDeadline: absolute time
    deadline := time.Now().Add(500 * time.Millisecond)
    ctx2, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()
    
    result2, _ := fetchFromAPI(ctx2, "api.example.com/orders")
    fmt.Println(result2)
}

The defer cancel() pattern ensures resources are released even when the operation completes before the timeout. Skipping this call causes a resource leak: the context's timer goroutine remains active until the timeout expires.

Request-Scoped Values with WithValue

WithValue attaches request-scoped data to a context. Common use cases include request IDs, authentication tokens, and tracing spans. The official documentation emphasizes using custom types as keys to avoid collisions.

context_values.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
)

// Define custom key types to avoid collisions
type contextKey string

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// middleware adds request ID to context
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }
        
        // Create new context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, requestID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// handler retrieves values from context
func handler(w http.ResponseWriter, r *http.Request) {
    requestID, ok := r.Context().Value(requestIDKey).(string)
    if !ok {
        requestID = "unknown"
    }
    
    log.Printf("[%s] Processing request", requestID)
    fmt.Fprintf(w, "Request ID: %s", requestID)
}

func generateRequestID() string {
    return fmt.Sprintf("req-%d", time.Now().UnixNano())
}

import "time"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    http.ListenAndServe(":8080", requestIDMiddleware(mux))
}

Context values form an immutable chain: each WithValue call creates a new context wrapping the parent. Lookups traverse this chain, making deeply nested values slower to access. Store only request-scoped data, not application configuration or optional parameters.

WithoutCancel and AfterFunc Utilities

Go 1.21 added WithoutCancel for operations that must complete regardless of parent cancellation, such as cleanup tasks. AfterFunc schedules a callback when context cancellation occurs.

utilities.gogo
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func saveAuditLog(ctx context.Context, message string) error {
    // Use WithoutCancel: audit logs must complete even if request canceled
    cleanCtx := context.WithoutCancel(ctx)
    
    // Simulate database write
    select {
    case <-time.After(50 * time.Millisecond):
        log.Printf("Audit: %s", message)
        return nil
    case <-cleanCtx.Done():
        // This branch never executes: WithoutCancel context is never canceled
        return cleanCtx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    
    // AfterFunc: register cleanup when context ends
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Context ended, running cleanup")
    })
    
    // Perform operation
    time.Sleep(50 * time.Millisecond)
    
    // Cancel stops the AfterFunc from running if called before context ends
    if stop() {
        fmt.Println("Cleanup prevented")
    }
    
    cancel()
    
    // Audit log completes despite parent cancellation
    saveAuditLog(ctx, "Operation completed")
}

WithoutCancel preserves the values from the parent context but ignores its cancellation signal. This pattern suits logging, metrics emission, and database commits that must succeed regardless of the request outcome.

Interview Questions and Answers

Preparing for a Go technical interview requires understanding context internals and best practices. These questions appear frequently in interviews at companies using Go for backend services. For more Go interview preparation, see the Context Package interview questions module.

Why should context be the first parameter?

The Go team established this convention to make context propagation visible and consistent. Placing context first signals to readers that the function respects cancellation. The standard library follows this pattern: http.Request.Context(), database/sql.QueryContext(), and grpc.UnaryInterceptor all expect context as the first argument.

What happens if you store context in a struct?

Storing context breaks the request lifecycle model. A struct might outlive the request it was created for, causing operations to use stale cancellation signals or miss new deadlines. The context documentation explicitly warns against this pattern.

go
// BAD: context stored in struct
type Service struct {
    ctx context.Context // Never do this
}

// GOOD: pass context to each method
type Service struct{}

func (s *Service) Process(ctx context.Context, data []byte) error {
    // Context flows through the call chain
    return s.save(ctx, data)
}

How do you handle context in goroutines?

Goroutines must check ctx.Done() in their main loop or select statement. Ignoring the done channel creates goroutine leaks: the parent function returns, but the goroutine continues consuming resources.

go
// Correct pattern for long-running goroutines
func processStream(ctx context.Context, stream <-chan Data) {
    for {
        select {
        case <-ctx.Done():
            log.Println("Shutting down processor")
            return
        case data, ok := <-stream:
            if !ok {
                return
            }
            handle(data)
        }
    }
}

When should you use context.TODO()?

Use TODO() during incremental refactoring when adding context support to legacy code. It marks locations that need proper context propagation. Production code should eventually replace all TODO() calls with actual contexts from request handlers or application initialization. For more on Go concurrency patterns, see Go Concurrency: Goroutines and Channels.

Common Mistakes to Avoid

Context misuse leads to subtle bugs that manifest under load or during shutdown sequences. Recognizing these patterns prevents production incidents.

mistakes.gogo
package main

import (
    "context"
    "time"
)

// MISTAKE 1: Ignoring context cancellation
func badWorker(ctx context.Context) {
    for {
        // Missing select on ctx.Done()
        doExpensiveWork() // Never stops when context canceled
    }
}

// MISTAKE 2: Not calling cancel
func leakyTimeout() {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    // Timer goroutine leaks until timeout expires
    _ = ctx
}

// MISTAKE 3: Using string keys for values
func collisionProne(ctx context.Context) context.Context {
    // Different packages might use same string key
    return context.WithValue(ctx, "userID", 123) // Bad
}

// CORRECT versions
func goodWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doExpensiveWork()
        }
    }
}

func properTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel() // Always release resources
    _ = ctx
}

type userIDKey struct{}

func collisionSafe(ctx context.Context) context.Context {
    return context.WithValue(ctx, userIDKey{}, 123) // Good
}

func doExpensiveWork() {}

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Key Takeaways for Using Go Context in Production

  • Pass context as the first parameter to functions, never store it in structs
  • Always defer cancel() after WithTimeout, WithDeadline, or WithCancel to prevent resource leaks
  • Use WithCancelCause and context.Cause() for better error debugging in complex cancellation chains
  • Check <-ctx.Done() in goroutine loops to enable graceful shutdown and prevent leaks
  • Use custom key types for WithValue to avoid collisions between packages
  • Apply WithoutCancel for cleanup operations that must complete regardless of parent cancellation
  • Prefer context.Background() for application initialization and context.TODO() only during refactoring
Daily challenge

Can you spot the bug in Go?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on August 29, 2026

Tags

#go
#golang
#context
#concurrency
#interview

Share

Related articles