Context Deeper β€” Deadlines, Real Auth & Design


Progress
0/5

🧩 Prerequisites: You’ve completed lesson 0005 (Context basics) β€” context.WithValue, r.WithContext(), type assertions, and the auth middleware pattern. This lesson builds on all of that.

🎯 Your mission: Context isn’t just a key-value bag. It handles deadlines and cancellation so your server doesn’t leak resources. It powers real auth middleware that reads bearer tokens and blocks unauthorized requests. And there’s a clear design philosophy about when context is the right tool vs. when plain arguments work better. By the end, you’ll know all three.

1. Deadlines & cancellation β€” don't leak resources

When a client disconnects or a query takes too long, your handler should stop working. context.WithTimeout and context.WithCancel create child contexts that signal β€œstop now.” Anything that accepts a context β€” database queries, HTTP calls, background goroutines β€” will respect that signal. Cancelling a parent cancels all children automatically. β†’ context.WithTimeout docs

2. Real auth middleware β€” tokens from headers

The fake auth from lesson 0005 hardcoded β€œalice”. Real auth reads r.Header.Get(β€œAuthorization”), strips the β€œBearer β€œ prefix, validates the token, and stores a user struct in context. If validation fails, the middleware returns 401 and never calls next.ServeHTTP β€” blocking the request completely. This is middleware’s superpower. β†’ http.Request.Header docs

3. Context vs. arguments β€” the design rule

Context is for request-scoped data that crosses API boundaries (middleware β†’ handler β†’ service). Function arguments are for everything else. Don’t stuff config or database connections into context β€” those belong in structs. The Go docs say it clearly: β€œUse context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.” β†’ context package overview

// The extended context universe β€” beyond WithValue

context.Background()           <span class="dim">// empty root</span>
  β”‚
  β”œβ”€β”€ .WithValue(k, v)  <span class="hl">β†’</span>  <span class="dim">stores a value (lesson 0005)</span>
  β”‚
  β”œβ”€β”€ .WithTimeout(d)   <span class="hl">β†’</span>  auto-cancels after duration d
  β”‚                           <span class="dim">ctx.Done() closes β†’ handler stops</span>
  β”‚
  └── .WithCancel()     <span class="hl">β†’</span>  cancel() function β†’ manual stop
                              <span class="dim">cancel() closes ctx.Done()</span>

// Each child shares its parent's cancellation:
//    parent cancelled β†’ all children cancelled too

Follow each step in order. Click Done βœ“ after completing it.

1Set up the lesson directoryCurrent
mkdir -p ~/dev/learn/go-lang/lessons/bonus-context-deeper
cd ~/dev/learn/go-lang/lessons/bonus-context-deeper
go mod init context-deeper
2Part 1: Deadlines & Cancellation β€” run a timeout experimentLocked

Context carries deadlines and cancellation signals through your program. When a deadline hits, case <-ctx.Done() fires β€” and any goroutine watching it can stop. Create timeout.go:

package main

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

// doWork simulates a slow operation (e.g. a database query).
// It watches ctx.Done() β€” if the context is cancelled, it bails out.
func doWork(ctx context.Context) (string, error) {
  select {
  case <-time.After(3 * time.Second):
      return "work completed", nil
  case <-ctx.Done():
      return "", fmt.Errorf("cancelled: %w", ctx.Err())
  }
}

func main() {
  // WithTimeout: auto-cancels after 1 second
  ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
  defer cancel() // always call cancel to free resources

  fmt.Println("Starting work with 1 second deadline...")
  result, err := doWork(ctx)
  if err != nil {
      fmt.Println("Error:", err)
  } else {
      fmt.Println("Result:", result)
  }

  // Bonus: simulate a client disconnect
  fmt.Println("\n--- WithCancel demo ---")
  ctx2, cancel2 := context.WithCancel(context.Background())
  go func() {
      time.Sleep(500 * time.Millisecond)
      cancel2() // cancel after half a second
  }()
  result2, err2 := doWork(ctx2)
  if err2 != nil {
      fmt.Println("Error:", err2)
  } else {
      fmt.Println("Result:", result2)
  }
}

Run it:

go run timeout.go

You’ll see cancelled: context deadline exceeded from the timeout, and cancelled: context canceled from the manual cancel. The select statement waits for whichever channel receives first β€” the 3-second work never completes because the context cancels earlier.

Key insight: ctx.Done() returns a channel. When the context is cancelled (by timeout, manual cancel, or client disconnect), that channel closes β€” making the case <-ctx.Done() branch fire. ctx.Err() tells you why. β†’ Tour of Go: Select

3Part 2: Real Auth Middleware β€” read tokens, store usersLocked

The fake auth from lesson 0005 hardcoded β€œalice”. Real middleware reads the Authorization header, validates a bearer token, and stores an AuthUser struct in context. Create main.go:

package main

import (
  "context"
  "log"
  "net/http"
  "strings"
)

// --- Context key and user type ---

type ctxKey string
const ctxKeyUser ctxKey = "user"

type AuthUser struct {
  ID    int
  Email string
}

// --- Real auth middleware ---

func jwtAuthMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // 1) Read the Authorization header
      authHeader := r.Header.Get("Authorization")

      // 2) Check it's a Bearer token
      if !strings.HasPrefix(authHeader, "Bearer ") {
          http.Error(w, "missing or invalid token", http.StatusUnauthorized)
          return // ← important: never call next.ServeHTTP
      }

      // 3) Strip prefix and validate
      token := strings.TrimPrefix(authHeader, "Bearer ")
      user, err := validateToken(token)
      if err != nil {
          http.Error(w, "invalid token", http.StatusUnauthorized)
          return
      }

      // 4) Store the user in context β†’ pass to next handler
      ctx := context.WithValue(r.Context(), ctxKeyUser, user)
      next.ServeHTTP(w, r.WithContext(ctx))
  })
}

// validateToken is a stub β€” real code would parse JWT, verify signature, check expiry.
func validateToken(token string) (*AuthUser, error) {
  if token == "secret-token" {
      return &AuthUser{ID: 42, Email: "alice@example.com"}, nil
  }
  return nil, http.ErrNoCookie // placeholder error
}

// --- Handler that reads the user ---

func whoami(w http.ResponseWriter, r *http.Request) {
  user, ok := r.Context().Value(ctxKeyUser).(*AuthUser)
  if !ok {
      http.Error(w, "unauthorized", http.StatusUnauthorized)
      return
  }
  w.Write([]byte("Hello, " + user.Email + "!\n"))
}

// --- Main ---

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("GET /whoami", whoami)
  mux.HandleFunc("GET /public", func(w http.ResponseWriter, r *http.Request) {
      w.Write([]byte("Anyone can access this.\n"))
  })

  // Only /whoami goes through auth middleware
  handler := jwtAuthMiddleware(mux)

  log.Println("Server starting on http://localhost:8080")
  log.Fatal(http.ListenAndServe(":8080", handler))
}
What changed from lesson 0005 auth?
  • Reads a real header: r.Header.Get(β€œAuthorization”)
  • Validates: checks for β€œBearer β€œ prefix, calls validateToken
  • Blocks on failure: returns 401 without calling next.ServeHTTP β€” the handler never runs
  • Stores a struct: not just a string β€” a typed *AuthUser with ID and email
  • Type assertion on the struct: r.Context().Value(key).(*AuthUser)
4Run and test real auth β€” with & without the tokenLocked

Start the server:

go run main.go

In a second terminal, test the auth:

# Without token β€” blocked by middleware, returns 401
curl -i http://localhost:8080/whoami

# With a valid token β€” handler runs, returns user info
curl -H "Authorization: Bearer secret-token" http://localhost:8080/whoami

# With an invalid token β€” blocked with 401
curl -H "Authorization: Bearer wrong" http://localhost:8080/whoami

# Public endpoint β€” no auth needed (but auth middleware still runs,
# it just doesn't find a header, so everyone hits 401 for now!)

Notice: the current jwtAuthMiddleware wraps the entire mux, so even /public requires a token. That’s fine for a demo, but in production you’d apply auth middleware only to protected routes. (We’ll cover router-level middleware grouping in a future lesson.)

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

5Part 3: Context vs. Arguments β€” the design ruleLocked

No code to write here β€” this is a design lesson. Read these guidelines carefully; they’ll save you from one of the most common Go mistakes.

Use context for...Use function arguments for...
Request-scoped data (user ID, trace ID, tenant)Business data your function operates on
Data that crosses API boundaries (middleware β†’ handler β†’ service layer)Data within the same package or call chain
Deadlines, cancellation, timeoutsConfiguration or dependencies (pass at startup)
Things every handler might need but should not explicitly declareEverything else β€” be explicit when you can

Concrete examples

// βœ… GOOD: user identity via context (request-scoped, crosses boundaries)
func getUserOrders(w http.ResponseWriter, r *http.Request) {
  user := r.Context().Value(ctxKeyUser).(*AuthUser)
  orders := fetchOrders(user.ID)    // explicit argument for business data
  json.NewEncoder(w).Encode(orders)
}

// βœ… GOOD: request ID in context (cross-cutting, every handler might log it)
ctx = context.WithValue(ctx, ctxKeyRequestID, uuid.New().String())

// βœ… GOOD: database connection in a struct field, not context
type Server struct {
  db *sql.DB
}

// ❌ BAD: passing context values as regular arguments defeats the purpose
func getUserOrders(user AuthUser) { ... } // where does user come from?

// ❌ BAD: stuffing dependencies into context
ctx = context.WithValue(ctx, "db", myDB)  // don't do this

The official Go docs put it best: β€œUse context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.” If you find yourself stuffing config or dependencies into context, put them in a struct instead. β†’ context package overview

Check your understanding

What does ctx.Done() return, and when does it become readable?

What happens if auth middleware finds an invalid token?

Should you store a database connection in context.Context?

What's the difference between context.WithTimeout and context.WithCancel?