Skip to Content
Course content

142: Passing Values Through Context

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you're probably comfortable using context.Context for cancellation and timeouts. But there's a second, more controversial use for it: passing values. I've seen a lot of developers treat the context like a giant bucket where they throw everything from database connections to user permissions. Please, don't do that. Context values are for request-scoped data—things that live and die with a single request, like a Request ID, a tracing token, or the authenticated user's ID.

Wiring up a Request ID middleware

Let's build a small example. Imagine we want every log line associated with a specific HTTP request to share a unique ID. This makes debugging a nightmare if you don't have it, but a breeze if you do. First, I'll write a middleware that generates a random ID and injects it into the request context.

package main

import (
	"context"
	"fmt"
	"net/http"
	"math/rand"
)

func RequestIDMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := fmt.Sprintf("%d", rand.Intn(100000))
		
		// I'm putting the ID into the context here
		ctx := context.WithValue(r.Context(), "requestID", id)
		
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func handler(w http.ResponseWriter, r *http.Request) {
	id := r.Context().Value("requestID")
	fmt.Fprintf(w, "Your request ID is: %v", id)
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("/", RequestIDMiddleware(http.HandlerFunc(handler)))
	http.ListenAndServe(":8080", mux)
}

The "Collision" mistake

If you ran the code above, it would work. But I just gave you a bad habit. I used a plain string "requestID" as the key. In a small project, that's fine. But imagine you're using a third-party library that also decides to store something in the context using the string "requestID". One of you will overwrite the other, and you'll spend three hours chasing a bug that makes no sense.

To fix this, we use a custom, unexported type for the key. Since the type itself is part of the identity of the key, a type key int in my package will never collide with a type key int in your package, even if they both have the value 0.

// Define a private type for the key
type contextKey int

// Define the specific key we'll use
const requestIDKey contextKey = 0

func RequestIDMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := fmt.Sprintf("%d", rand.Intn(100000))
		
		// Now we use the typed key instead of a string
		ctx := context.WithValue(r.Context(), requestIDKey, id)
		
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Extracting values safely in the service layer

Now, we don't want our business logic to be littered with r.Context().Value(...) calls and type assertions. That's messy and fragile. The professional way to handle this is to write a helper function that encapsulates the retrieval and the type casting.

I'll create a GetRequestID function. This keeps the contextKey internal to the package and provides a clean API for the rest of the app.

func GetRequestID(ctx context.Context) string {
	if id, ok := ctx.Value(requestIDKey).(string); ok {
		return id
	}
	return "unknown" // Provide a sensible default
}

func processOrder(ctx context.Context) {
	// I can now grab the ID without knowing about the internal key type
	id := GetRequestID(ctx)
	fmt.Printf("[Request %s] Processing order in database...\n", id)
}

func handler(w http.ResponseWriter, r *http.Request) {
	processOrder(r.Context())
	fmt.Fprint(w, "Order processed!")
}

By wrapping the value extraction, you've created a type-safe bridge. If you ever decide to change how the Request ID is stored (maybe changing it from a string to a custom struct), you only have to change the code in one place—the helper function—rather than hunting down every .Value() call in your codebase.




📋 Practical Task

Implementing a User-Role Auth Context

Build a small program that simulates a role-based access system using context.WithValue. Your task is to:

  • Create a private contextKey type to avoid collisions.
  • Write a middleware (or a wrapper function) that injects a "userRole" (string) into the context based on a simulated user lookup.
  • Implement a helper function GetUserRole(ctx context.Context) string that safely retrieves the role or returns "guest" if none is found.
  • Create a final handler function that checks the role via the helper and prints "Welcome Admin" if the role is "admin", and "Welcome User" otherwise.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.