Context & Passing Data from Middleware


Progress
0/5

🧩 Prerequisites: You should be comfortable with middleware (lesson 0004) — the wrapping pattern, http.Handler, and next.ServeHTTP(w, r). This lesson builds directly on that.

🎯 Your mission: In lesson 0004, you learned how middleware wraps every request. But a real API needs middleware to talk to handlers — e.g., auth middleware extracting a user ID, then passing it to the route handler that needs it. Go does this with context.Context, a built-in data bag that rides along with every HTTP request. By the end, you’ll write an auth middleware that adds a username to the request, and a handler that reads it.

context.Context — a bag that travels with the request

Think of context.Context as an invisible backpack that every HTTP request carries. You can put things in it (like “which user made this request”) and take them out later. It also handles deadlines, cancellation signals, and timeouts — but for this lesson, we’re only focused on storing and retrieving values. → context package docs

How to get the context: r.Context()

Every *http.Request already has a context. You grab it with r.Context(). But you can’t modify the context directly — Go contexts are immutable. To add a value, you create a new context with context.WithValue(), and then create a new request with r.WithContext(newCtx). → Request.Context docs

The three-step dance

  1. ctx := r.Context() — get the current context
  2. ctx = context.WithValue(ctx, key, value) — create a new context with a value added
  3. r = r.WithContext(ctx) — create a new request that carries the updated context
    Then pass this updated request to next.ServeHTTP(w, r).

Getting values out: type assertion

ctx.Value(key) returns interface{} — Go’s generic “any type” container. To use it, you type-assert: val, ok := ctx.Value(key).(string). If ok is true, val is a string. If ok is false, the value wasn’t a string (or didn’t exist). Always check ok — skipping it can panic. → Tour of Go: Type assertions

Custom key types: why not just use strings?

If two packages both use "userID" as a key, they’ll collide — one overwrites the other. The convention is to define a custom type: type contextKey string. Each package has its own type, so keys can never collide. → context.WithValue docs (see “custom type” recommendation)

// The problem: middleware and handlers need to share data

<span class="dim">// ❌ Can't do this — handlers don't have access to middleware variables</span>
func authMiddleware(next) {
  userID := "42"       <span class="dim">// ← created here...</span>
  next(w, r)           <span class="dim">// ← ...but how does the handler get it?</span>
}

<span class="hl">// ✅ The solution: context.Context</span>

func authMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w, r) {
      <span class="dim">// Step 1: Get the current context from the request</span>
      ctx := r.Context()

      <span class="dim">// Step 2: Create a new context with our value added</span>
      ctx = context.WithValue(ctx, ctxKeyUserID, "42")

      <span class="dim">// Step 3: Create a new request carrying the updated context</span>
      r = r.WithContext(ctx)

      <span class="dim">// Pass the enriched request to the next handler</span>
      next.ServeHTTP(w, r)
  })
}

func getUserById(w, r) {
  <span class="dim">// Later, in any handler down the chain:</span>
  userID := r.Context().Value(ctxKeyUserID).(string)
  <span class="dim">// use userID...</span>
}
StepFunctionWhat it does
1. Get contextr.Context()Returns the context.Context attached to this request
2. Add valuecontext.WithValue(ctx, key, val)Returns a <em>new</em> context with key→val added (immutable!)
3. New requestr.WithContext(ctx)Returns a <em>new</em> request carrying the updated context
4. Read valuer.Context().Value(key)Looks up a value; returns interface{}, must type-assert

Follow each step in order. Click Done ✓ after completing it.

1Set up the lesson directoryCurrent
mkdir -p ~/dev/learn/go-lang/lessons/0005-context
cd ~/dev/learn/go-lang/lessons/0005-context
go mod init context-demo
2Understand context using a tiny standalone programLocked

Before wiring context into HTTP middleware, let’s see it in isolation. Create play.go and run it:

package main

import (
  "context"
  "fmt"
)

// A custom key type — prevents collisions between packages.
type contextKey string

func main() {
  // 1) Every context starts from the "background" — an empty root.
  ctx := context.Background()

  // 2) WithValue creates a NEW context (the old one is unchanged).
  userIDKey := contextKey("userID")
  ctx = context.WithValue(ctx, userIDKey, 42)

  // 3) Value() returns interface{} — use type assertion to get the int.
  val, ok := ctx.Value(userIDKey).(int)
  if !ok {
      fmt.Println("key not found!")
      return
  }
  fmt.Printf("userID = %d\n", val)

  // 4) What if we use the wrong type in the assertion?
  _, ok2 := ctx.Value(userIDKey).(string) // stored as int, asserting as string
  fmt.Printf("ok2 = %v (expected false)\n", ok2)

  // 5) What if we look up a key that was never stored?
  _, ok3 := ctx.Value(contextKey("missing")).(int)
  fmt.Printf("ok3 = %v (expected false)\n", ok3)
}

Run it:

go run play.go

Expected output: userID = 42, ok2 = false, ok3 = false. The type assertion acts as a safety check — it returns false instead of panicking.

3Study the auth middleware pattern (read, don't type yet)Locked

Now the key pattern. This middleware pretends to authenticate a request and passes the username into the context. Read this carefully — especially the three-step dance (r.Context()WithValuer.WithContext):

// Custom type for context keys — prevents collisions.
type contextKey string
const ctxKeyUsername contextKey = "username"

func authMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // 1) Get the existing context from the request
      ctx := r.Context()

      // 2) Create a new context with our value added
      //    (real auth would parse a token/header here)
      ctx = context.WithValue(ctx, ctxKeyUsername, "alice")

      // 3) Create a new request that carries the updated context
      r = r.WithContext(ctx)

      // 4) Pass the enriched request to the next handler
      next.ServeHTTP(w, r)
  })
}

The handler reads it back with r.Context().Value():

func whoami(w http.ResponseWriter, r *http.Request) {
  username, ok := r.Context().Value(ctxKeyUsername).(string)
  if !ok {
      http.Error(w, "unauthorized", http.StatusUnauthorized)
      return
  }
  fmt.Fprintf(w, "Hello, %s!\n", username)
}

Notice: the middleware and the handler share ctxKeyUsername — they both know the key. The middleware writes with WithValue, the handler reads with Value. This is the full pattern.

4Write the full server with auth middlewareLocked

Create main.go in the lesson directory. This server has two handlers: one that requires auth (reads the username from context), and one that just lists users (no auth). The logging middleware from lesson 0004 is layered on top:

package main

import (
  "context"
  "encoding/json"
  "fmt"
  "log"
  "net/http"
  "time"
)

// --- Context keys ---

type contextKey string
const ctxKeyUsername contextKey = "username"

// --- Middleware ---

func loggingMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      start := time.Now()
      log.Printf("→ %s %s", r.Method, r.URL.Path)
      next.ServeHTTP(w, r)
      log.Printf("← %s %s (%v)", r.Method, r.URL.Path, time.Since(start))
  })
}

func authMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // Pretend we authenticated the user.
      // Real code would check an Authorization header here.
      ctx := context.WithValue(r.Context(), ctxKeyUsername, "alice")
      next.ServeHTTP(w, r.WithContext(ctx))
  })
}

// --- Data ---

type User struct {
  ID        int       `json:"id"`
  Name      string    `json:"name"`
  Email     string    `json:"email"`
  CreatedAt time.Time `json:"created_at"`
}

var users = []User{
  {ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
  {ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
}

// --- Handlers ---

func getUsers(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(users)
}

func whoami(w http.ResponseWriter, r *http.Request) {
  // Read the username that authMiddleware stored in the context.
  username, ok := r.Context().Value(ctxKeyUsername).(string)
  if !ok {
      http.Error(w, "unauthorized", http.StatusUnauthorized)
      return
  }
  fmt.Fprintf(w, "Hello, %s!\n", username)
}

// --- Main ---

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /whoami", whoami)
  mux.HandleFunc("GET /users", getUsers)

  // Layer: auth → logging → mux
  // The onion: authMiddleware runs first, then loggingMiddleware, then mux.
  handler := authMiddleware(loggingMiddleware(mux))

  log.Println("Server starting on http://localhost:8080")
  log.Fatal(http.ListenAndServe(":8080", handler))
}
What’s new this lesson?
  • context.WithValue(ctx, key, val) — creates a new context with a value stored
  • r.WithContext(ctx) — creates a new request carrying the updated context
  • r.Context().Value(key).(string) — reads a value and type-asserts it
  • type contextKey string — custom key type to avoid collisions
  • Multiple middleware chained: authMiddleware(loggingMiddleware(mux))
5Run and test the context in actionLocked

Start the server:

go run main.go

In a second terminal, test both endpoints:

# /whoami reads the username from context (placed by auth middleware)
curl http://localhost:8080/whoami

# /users doesn't use context — it still works fine
curl http://localhost:8080/users

You should see Hello, alice! from /whoami. The auth middleware placed “alice” into the context, and the handler read it back. The logging middleware still logs everything. Both middleware are working independently.

Press Ctrl+C to stop the server when you’re done.

Check your understanding

Why do we call r.WithContext(ctx) instead of just modifying r.Context() directly?

What does the ok in val, ok := ctx.Value(key).(string) tell you?

What problem does type contextKey string solve?

In the server from step 4, what would /whoami return if the authMiddleware was removed from the middleware chain?

🎉 You finished the lesson!

Context also handles deadlines, cancellation, real auth token parsing, and design trade-offs.

Go deeper →