Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
138: Context Propagation in HTTP Handlers
If you've been building HTTP servers in Go, you've probably noticed that http.Request carries a Context. For a while, it feels like boilerplate—something you just pass along because the function signature requires it. But context propagation is actually your primary tool for preventing "ghost" work from eating your server's resources.
Imagine a user hits an endpoint that triggers a heavy database query. Halfway through, the user gets impatient and closes their browser tab. Without proper propagation, your server will keep churning away on that query until it finishes, even though there's no one left to receive the answer. That's a waste of CPU and memory.
Simulating a slow data fetch
Let's build a small example. We'll create a handler that fetches a user's profile. To make it realistic, I'll simulate a database call that takes a few seconds to complete. I want this function to be "context-aware," meaning it should stop working the moment the request is cancelled.
type User struct {
ID int
Name string
}
func fetchUserFromDB(ctx context.Context, id int) (*User, error) {
// Simulate a slow DB query with a timer
select {
case <-time.After(2 * time.Second):
return &User{ID: id, Name: "Jane Doe"}, nil
case <-ctx.Done():
// This is the magic part: we stop if the context is cancelled
return nil, ctx.Err()
}
}
The mistake I almost made
Now, I need to call this from my HTTP handler. Here is where I often slipped up early in my career. I'll write the handler, but I'm going to intentionally do it wrong first. I'll use context.Background() inside the handler instead of the request's own context.
func userHandler(w http.ResponseWriter, r *http.Request) {
id := 123
// WRONG: I'm creating a fresh context instead of using the request's context
user, err := fetchUserFromDB(context.Background(), id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "User: %s", user.Name)
}
At first glance, this works perfectly. If you hit the endpoint, you get the user back after two seconds. But there's a hidden bug. If you trigger this request and then immediately hit "Stop" in your browser, the server keeps running fetchUserFromDB for the full two seconds. I've effectively severed the link between the client's connection and the backend work.
Connecting the dots with r.Context()
To fix this, I need to propagate the context that Go's net/http package already manages for me. Every incoming request comes with a context that is automatically cancelled the moment the client disconnects or the connection times out.
I'll swap out context.Background() for r.Context(). This is the core of context propagation: passing the context from the outermost layer (the handler) down to the innermost layer (the database driver or API client).
func userHandler(w http.ResponseWriter, r *http.Request) {
id := 123
// RIGHT: we pass the request context down the chain
user, err := fetchUserFromDB(r.Context(), id)
if err != nil {
// If the user cancelled, ctx.Err() will return context.Canceled
log.Printf("Request cancelled or failed: %v", err)
return
}
fmt.Fprintf(w, "User: %s", user.Name)
}
Now, if you test this and kill the request mid-flight, you'll see the log.Printf trigger almost immediately. The select block in fetchUserFromDB catches the ctx.Done() signal, stops the timer, and returns. No wasted cycles.
A quick word on middleware
In a real production app, you wouldn't just rely on the client disconnecting. You'd likely use a middleware to wrap r.Context() with a timeout (using context.WithTimeout). This ensures that even if the client stays connected, a hung database query won't hold a request open for ten minutes. The propagation pattern remains exactly the same: you create a derived context in the middleware, and it flows down into your handlers and services.
📋 Practical Task
Build a Request-Aware Weather and News Aggregator
You are building a dashboard handler that fetches data from two simulated external services: a Weather API and a News API. Both are slow.
Your task:
- Create two functions,
getWeather(ctx context.Context)andgetNews(ctx context.Context). Both should simulate a 3-second delay usingtime.Afterand aselectblock that listens forctx.Done(). - Create an HTTP handler that calls both functions.
- Ensure that you propagate the request context (
r.Context()) into both service calls. - Test your implementation: start the request and cancel it (or use a tool like
curlwith a max-time limit). Verify via logs that both services stop processing immediately when the request is cancelled.
There are no comments for now.