In modern Go applications, managing deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines is a necessity.

What is Context in Go?


In modern Go applications, managing deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines is a necessity. The context package in Go provides a standard way to handle these requirements. In this blog post, we’ll delve into the context package, explore best practices, and learn how to test functions that use the context.

The context package provides a way to carry deadlines, cancellation signals, and other request-scoped values across API boundaries and between goroutines. It helps prevent resource leaks when goroutines are no longer needed as we can use it to cancel and clean up.

Using Context in Go Programs

The primary type in the context package is context.Context. It’s an interface that carries deadlines, cancellation signals, and other values across API boundaries.

Creating a Background Context

The root of any context tree is the background context:

ctx := context.Background()

Passing Context to functions

Context should be the first parameter in function signatures. Whilst it can technically doesn’t need to be first, this is where the Go community has landed on as best practise, so you’ll see a lot of code that looks like this:

func DoAThing(ctx context.Context, arg string) error {
    // Function implementation
}

Storing Values in Context

You can store values in a context using context.WithValue, but it’s generally discouraged except for request-scoped data. Its very easy for the context to become a bit of a dumping ground, so use it carefully!

type key string

func DoAThing(ctx context.Context) {
userID := ctx.Value(key("userID")).(int)
fmt.Println("User ID:", userID)
}

Context within Goroutines

When launching goroutines, you can pass a context to ensure it can be cancelled.

func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Goroutine cancelled")
                return
            default:
                // Do some work
            }
        }
    }(ctx)

    // Simulate some work
    time.Sleep(2 * time.Second)
    cancel()
}

Cancellations and Timeouts

Cancelling Contexts is very valuable. Let’s see how to do that.

Using context.WithCancel

Create a new context that is automatically canceled after a specified duration. This can be useful to ensure that things like http requests don’t last too long.

ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()

Handling Cancellation

Always check for cancellation by monitoring ctx.Done().

select {
case <-ctx.Done():
    // Handle cancellation
    return ctx.Err()
default:
    // Continue processing
}

Best Practices for Using Context in Go

  1. Pass Context Explicitly: Always pass the context explicitly as the first parameter to functions.

  2. Don’t Store Contexts in Structs: Contexts are meant to be passed down the call chain, not stored.

  3. Don’t Pass nil Context: Always provide a valid context. If you don’t have one, use context.Background().

  4. Use context.Value Sparingly: It’s meant for request-scoped data that transits processes and APIs.

  5. Handle Cancellation Properly: Always check ctx.Done() to handle cancellations.

  6. Set Deadlines Where Appropriate: Use timeouts and deadlines to prevent goroutines from running indefinitely.

Testing Functions that Use a Context

Testing functions that accept a context.Context can be straightforward. You can use context.Background() in tests, or create contexts with timeouts and cancellation as needed.

Example: Testing with a Timeout Context

func TestDoSomething(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

    err := DoSomething(ctx)
    if err != nil {
        t.Errorf("DoSomething failed: %v", err)
    }
}

Example: Using context.WithCancel in Tests

func TestDoSomethingCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately

    err := DoSomething(ctx)
    if err != context.Canceled {
        t.Errorf("Expected context.Canceled, got %v", err)
    }
}

Wrapping Up

Thanks for reading our blog on the context in Go! I hope you have found it useful. You can read more about the context on the official Go Blog and see the package documentation here.

Be sure to take a look at other blogs here and subscribe to our newsletter to learn many more great Go tips.