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
100: The Decorator Pattern via Middleware
When I first started seeing people use frameworks like Gin or Echo, I noticed a recurring trend: developers treating "Middleware" as if it were a proprietary feature of the framework itself. I've seen countless juniors ask, "Which framework should I use to get the best middleware support?"
Here is the reality: Middleware isn't a framework feature. It's just a practical application of the Decorator Pattern using Go's first-class functions. If you think you need a library to handle "cross-cutting concerns" like logging or authentication, you're overcomplicating your stack.
Middleware isn't a Framework Feature; it's just a Function Signature
In Go, the net/http package gives us the http.Handler interface. Most of us spend our time writing functions that satisfy this interface. The "magic" of middleware happens when you realize that a middleware is simply a function that takes an http.Handler as an input and returns a new http.Handler as an output.
Look at this signature:
type Middleware func(http.Handler) http.Handler
That's it. That's the whole secret. The new handler returned by the middleware "wraps" the original one. It can do something before calling the original handler, something after, or even decide not to call the original handler at all (which is exactly how authentication guards work).
Stop Hardcoding Logic in Every Handler; Wrap the Behavior Instead
Imagine you're building a user profile API. You need to log every request for auditing and check if the request has a valid API key. The "naive" way is to paste that logic at the top of every single handler function. It's repetitive, it's brittle, and it's a nightmare to maintain. I've spent far too many hours scrubbing duplicate auth logic out of legacy codebases to recommend this approach.
Instead, we treat the core logic (fetching the user) as the "component" and the logging/auth as "decorators." Here is how that looks in practice:
package main
import (
"fmt"
"log"
"net/http"
)
// This is our core logic. It doesn't know or care about logging or auth.
func userProfileHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Welcome to your profile page!")
}
// LoggingMiddleware is a decorator. It adds logging behavior around any handler.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request received: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r) // Call the original handler
log.Printf("Request completed")
})
}
// AuthMiddleware is another decorator. It can stop the chain entirely.
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
if apiKey != "secret-token-123" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return // We stop here; 'next' is never called.
}
next.ServeHTTP(w, r)
})
}
func main() {
// Wrap the core handler in multiple layers of decorators
finalHandler := http.HandlerFunc(userProfileHandler)
// Order matters! We log first, then check auth.
wrappedHandler := LoggingMiddleware(AuthMiddleware(finalHandler))
http.ListenAndServe(":8080", wrappedHandler)
}
I like to think of this as an onion. The core logic is at the center. Each middleware is a layer you wrap around it. When a request comes in, it hits the outermost layer first (Logging), moves inward (Auth), hits the center (User Profile), and then bounces back out through the layers.
One quick tip: as your chain grows, Middleware(Middleware(Middleware(handler))) gets ugly fast. In a real project, you'll likely write a small helper function to slice through a list of middlewares, but the underlying principle remains the same: you are decorating a function with more behavior.
📋 Practical Task
Building a Request-Timing and Panic-Recovery Pipeline
Your goal is to expand the middleware pattern to make your server more resilient and observable. You will build a pipeline that wraps a simple "Hello" handler with two specific decorators:
- The Timer Middleware: It should capture the time when a request starts and, after the inner handler finishes, calculate the duration and print it to the console (e.g.,
"Request took 12ms"). - The Recovery Middleware: This is critical for production. It should use
deferandrecover()to catch any panics that happen in the inner handlers. Instead of letting the whole server crash, it should recover and return a500 Internal Server Errorto the client.
Requirements:
- Create a handler that randomly panics (use
rand.Intn) to test your recovery middleware. - Ensure the Timer middleware correctly reports the duration even if the inner handler panics (Hint: the timer should be stopped in a
deferblock). - Chain them so that the Recovery middleware is the outermost layer, ensuring it catches panics from both the Timer middleware and the final handler.
There are no comments for now.