Skip to main content
โšก Calmops

Context Package: Cancellation and Timeouts

Context Package: Cancellation and Timeouts

The context package is essential for managing cancellation, timeouts, and deadlines in Go applications. It provides a clean way to propagate cancellation signals through your codebase.

Context Basics

Context carries deadlines, cancellation signals, and request-scoped values.

Good: Using Context

package main

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

func doWork(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Work cancelled:", ctx.Err())
			return
		default:
			fmt.Println("Working...")
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	
	go doWork(ctx)
	
	time.Sleep(500 * time.Millisecond)
	cancel()
	
	time.Sleep(100 * time.Millisecond)
}

Bad: Manual Cancellation

// โŒ AVOID: Manual cancellation without context
package main

import (
	"fmt"
	"time"
)

func doWork(done chan bool) {
	for {
		select {
		case <-done:
			fmt.Println("Work cancelled")
			return
		default:
			fmt.Println("Working...")
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func main() {
	done := make(chan bool)
	
	go doWork(done)
	
	time.Sleep(500 * time.Millisecond)
	close(done)
	
	time.Sleep(100 * time.Millisecond)
}

Context Types

WithCancel

Cancel context manually.

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
	defer wg.Done()
	
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("Worker %d stopped\n", id)
			return
		default:
			fmt.Printf("Worker %d working\n", id)
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	
	var wg sync.WaitGroup
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go worker(ctx, i, &wg)
	}
	
	time.Sleep(500 * time.Millisecond)
	cancel()
	
	wg.Wait()
}

WithTimeout

Automatically cancel after duration.

package main

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

func fetchData(ctx context.Context) (string, error) {
	select {
	case <-time.After(2 * time.Second):
		return "data", nil
	case <-ctx.Done():
		return "", ctx.Err()
	}
}

func main() {
	// Timeout after 1 second
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	
	data, err := fetchData(ctx)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Data:", data)
	}
}

WithDeadline

Cancel at specific time.

package main

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

func doWork(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Deadline exceeded:", ctx.Err())
			return
		default:
			fmt.Println("Working...")
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func main() {
	deadline := time.Now().Add(500 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), deadline)
	defer cancel()
	
	go doWork(ctx)
	
	time.Sleep(1 * time.Second)
}

WithValue

Attach request-scoped values.

package main

import (
	"context"
	"fmt"
)

type userKey string

const userIDKey userKey = "userID"

func processRequest(ctx context.Context) {
	userID := ctx.Value(userIDKey)
	fmt.Printf("Processing request for user: %v\n", userID)
}

func main() {
	ctx := context.WithValue(context.Background(), userIDKey, 12345)
	processRequest(ctx)
}

Propagating Context

Pass context through function calls.

Good: Context Propagation

package main

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

func level3(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("Level 3: Context cancelled")
		return
	case <-time.After(100 * time.Millisecond):
		fmt.Println("Level 3: Work complete")
	}
}

func level2(ctx context.Context) {
	fmt.Println("Level 2: Starting")
	level3(ctx)
	fmt.Println("Level 2: Done")
}

func level1(ctx context.Context) {
	fmt.Println("Level 1: Starting")
	level2(ctx)
	fmt.Println("Level 1: Done")
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()
	
	level1(ctx)
}

Bad: Not Propagating Context

// โŒ AVOID: Not passing context through call chain
package main

import (
	"fmt"
	"time"
)

func level3() {
	time.Sleep(100 * time.Millisecond)
	fmt.Println("Level 3: Work complete")
}

func level2() {
	fmt.Println("Level 2: Starting")
	level3()
	fmt.Println("Level 2: Done")
}

func level1() {
	fmt.Println("Level 1: Starting")
	level2()
	fmt.Println("Level 1: Done")
}

func main() {
	// No way to cancel level3 work
	level1()
}

Timeout Patterns

HTTP Request with Timeout

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"time"
)

func fetchURL(ctx context.Context, url string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return "", err
	}
	
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	
	body, err := io.ReadAll(resp.Body)
	return string(body), err
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	
	data, err := fetchURL(ctx, "https://example.com")
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Printf("Fetched %d bytes\n", len(data))
	}
}

Database Query with Timeout

package main

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

// Simulated database query
func queryDatabase(ctx context.Context, query string) (string, error) {
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case <-time.After(2 * time.Second):
		return "result", nil
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	
	result, err := queryDatabase(ctx, "SELECT * FROM users")
	if err != nil {
		fmt.Println("Query error:", err)
	} else {
		fmt.Println("Result:", result)
	}
}

Goroutine Coordination

Use context to coordinate multiple goroutines.

Good: Coordinated Cancellation

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
	defer wg.Done()
	
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("Worker %d: Cancelled\n", id)
			return
		case <-time.After(100 * time.Millisecond):
			fmt.Printf("Worker %d: Working\n", id)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go worker(ctx, i, &wg)
	}
	
	time.Sleep(500 * time.Millisecond)
	fmt.Println("Cancelling all workers...")
	cancel()
	
	wg.Wait()
	fmt.Println("All workers stopped")
}

Context with Values

Good: Request-Scoped Values

package main

import (
	"context"
	"fmt"
)

type requestIDKey string

const requestID requestIDKey = "requestID"

func handleRequest(ctx context.Context) {
	id := ctx.Value(requestID)
	fmt.Printf("Handling request: %v\n", id)
	
	processData(ctx)
}

func processData(ctx context.Context) {
	id := ctx.Value(requestID)
	fmt.Printf("Processing data for request: %v\n", id)
}

func main() {
	ctx := context.WithValue(context.Background(), requestID, "req-12345")
	handleRequest(ctx)
}

Bad: Global State

// โŒ AVOID: Using global variables for request data
package main

import (
	"fmt"
)

var currentRequestID string

func handleRequest(id string) {
	currentRequestID = id
	fmt.Printf("Handling request: %s\n", currentRequestID)
	processData()
}

func processData() {
	fmt.Printf("Processing data for request: %s\n", currentRequestID)
}

func main() {
	handleRequest("req-12345")
}

Error Handling with Context

Handling Context Errors

package main

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

func doWork(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-time.After(2 * time.Second):
		return nil
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	
	err := doWork(ctx)
	if err != nil {
		switch err {
		case context.Canceled:
			fmt.Println("Work was cancelled")
		case context.DeadlineExceeded:
			fmt.Println("Work exceeded deadline")
		default:
			fmt.Println("Error:", err)
		}
	}
}

Best Practices

  1. Always Pass Context: Pass context as first parameter
  2. Respect Cancellation: Check context.Done() regularly
  3. Use Appropriate Timeout: Set reasonable timeouts
  4. Don’t Store Context: Don’t store context in structs
  5. Create Child Contexts: Use WithCancel, WithTimeout, etc.
  6. Handle Errors: Check for context.Canceled and context.DeadlineExceeded
  7. Use Values Sparingly: Only for request-scoped data
  8. Defer Cancel: Always defer cancel() to prevent leaks

Common Pitfalls

  • Ignoring Context: Not checking context.Done()
  • Storing Context: Storing context in struct fields
  • No Timeout: Not setting timeouts for operations
  • Leaking Goroutines: Not cancelling context properly
  • Overusing Values: Using context for too much data

Resources

Summary

The context package is essential for managing cancellation, timeouts, and request-scoped values in Go. Always pass context as the first parameter, respect cancellation signals, and set appropriate timeouts. Use context to coordinate goroutines and propagate cancellation through your application gracefully.

Comments