Youssef Ameachaq's Blog

Youssef Ameachaq

Understanding Context in Go


In Go, the context package provides a way to carry deadlines, cancellation signals, and other request-scoped values across API boundaries. It is commonly used in applications where you need to manage timeouts, cancellations, or pass additional information between functions or goroutines.

Here’s an easy and detailed explanation with documented code examples.


Why context?

When you’re dealing with goroutines or HTTP requests, you may need:

  1. Cancellation: Stop ongoing work when a parent operation is canceled (e.g., user closed the browser tab).
  2. Timeout: Limit how long an operation can run.
  3. Shared Values: Pass values like user IDs or configurations without changing function signatures too much.

The context package helps solve these problems.


Basics of context

The context package provides the following:

  1. Context types:
    • context.Background(): The root context, often used as the starting point.
    • context.TODO(): A placeholder context when you’re unsure what to use.
  2. Derived contexts:
    • WithCancel: To cancel operations manually.
    • WithTimeout: To cancel after a set timeout.
    • WithDeadline: To cancel at a specific time.
    • WithValue: To carry values through the context.

Example 1: Basic context Usage with Cancellation

Suppose you want to stop a goroutine when a user cancels an operation.

package main

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

func main() {
	// Create a context with cancellation
	ctx, cancel := context.WithCancel(context.Background())

	go func() {
		for {
			select {
			case <-ctx.Done(): // Check if the context is canceled
				fmt.Println("Operation canceled!")
				return
			default:
				fmt.Println("Working...")
				time.Sleep(500 * time.Millisecond)
			}
		}
	}()

	time.Sleep(2 * time.Second)
	cancel() // Cancel the context
	time.Sleep(1 * time.Second) // Allow goroutine to finish
}

Explanation:


Example 2: Using WithTimeout

You can set a timeout for an operation, automatically canceling it when time runs out.

package main

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

func main() {
	// Create a context with a 2-second timeout
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel() // Clean up resources

	go func() {
		select {
		case <-ctx.Done():
			fmt.Println("Timeout reached:", ctx.Err())
		}
	}()

	time.Sleep(3 * time.Second)
	fmt.Println("Main function ends")
}

Explanation:


Example 3: Passing Values with Context

You can use WithValue to pass data through the context.

package main

import (
	"context"
	"fmt"
)

func main() {
	// Add a value to the context
	ctx := context.WithValue(context.Background(), "userID", 42)

	printUserID(ctx)
}

func printUserID(ctx context.Context) {
	// Retrieve the value from the context
	userID := ctx.Value("userID")
	if userID != nil {
		fmt.Println("User ID:", userID)
	} else {
		fmt.Println("No User ID found")
	}
}

Explanation:

⚠️ Note: Avoid overusing WithValue as it can make code harder to understand and debug.


Summary of Best Practices

  1. Use context responsibly: Don’t pass context to everywhere unnecessarily.
  2. Don’t store large data: Context should not be used for large objects like files.
  3. Pass context as the first parameter: Follow Go conventions (func doSomething(ctx context.Context, ...)).

Additional Resources