Context Deeper β Deadlines, Real Auth & Design
π§© 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 tooFollow each step in order. Click Done β after completing it.
mkdir -p ~/dev/learn/go-lang/lessons/bonus-context-deeper
cd ~/dev/learn/go-lang/lessons/bonus-context-deeper
go mod init context-deeperContext 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.goYouβ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
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
*AuthUserwith ID and email - Type assertion on the struct:
r.Context().Value(key).(*AuthUser)
Start the server:
go run main.goIn 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.
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, timeouts | Configuration or dependencies (pass at startup) |
| Things every handler might need but should not explicitly declare | Everything 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 thisThe 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?