Middleware as Decorator
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 97 of 107.
Middleware is one of the most practical applications of the Decorator pattern in Go. In web development, middleware functions wrap HTTP handlers to add cross-cutting concerns like logging, authentication, or timing without modifying the original handler.
The key insight is that middleware takes a handler and returns a new handler with added behavior. This follows the Decorator pattern exactly—same interface in and out, with enhanced functionality:
type Handler func(request string) string
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}Each middleware wraps the next handler in the chain, adding its behavior before or after calling the wrapped handler. You can stack multiple middleware by composing them:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Response to: GET /usersThe order of wrapping determines the execution order—the outermost middleware runs first. This pattern is used extensively in Go web frameworks for request processing pipelines, making it easy to add or remove functionality without touching core handler logic.
Challenge
EasyLet's build a request processing pipeline using the Middleware as Decorator pattern! You'll create a system where middleware functions wrap handlers to add functionality like timing, validation, and formatting—all without modifying the core handler logic.
You'll organize your code across three files:
handler.go: Define your handler type and the base handler that processes requests.Create a
Handlertype as a function that takes a request string and returns a response string:type Handler func(request string) stringImplement a
BaseHandlerfunction that returnsProcessed: [request]for any request it receives.middleware.go: Create your middleware functions that wrap handlers with additional behavior.Build three middleware functions, each taking a
Handlerand returning a newHandler:TimingMiddleware— wraps the response with timing info, returning[TIMING] [response]ValidationMiddleware— adds validation prefix, returning[VALID] [response]UppercaseMiddleware— converts the entire response to uppercase
Each middleware should call the wrapped handler and enhance its result.
main.go: Build a processing pipeline by composing middleware.Read the number of middleware layers to apply. Then for each layer, read the middleware type (
timing,validation, oruppercase) and wrap your current handler with that middleware. Apply them in the order they're read—the first middleware read becomes the outermost wrapper.After building the pipeline, read the request string, process it through your composed handler, and print the result.
The following inputs will be provided:
- Line 1: Number of middleware layers
- Following lines: One middleware type per line (
timing,validation, oruppercase) - Last line: The request to process
For example, given:
2
timing
validation
GET /usersYour output should be:
[TIMING] [VALID] Processed: GET /usersAnd given:
3
uppercase
timing
validation
POST /dataYour output should be:
[TIMING] [VALID] PROCESSED: POST /DATAAnd given:
1
uppercase
hello worldYour output should be:
PROCESSED: HELLO WORLDAnd given:
0
simple requestYour output should be:
Processed: simple requestNotice how the middleware wrapping order affects the output—the first middleware applied (outermost) runs first and sees the final result from all inner middleware. The uppercase middleware in the second example runs last (innermost), so it uppercases the base response before timing and validation add their prefixes!
Cheat sheet
Middleware in Go follows the Decorator pattern by wrapping handlers to add functionality without modifying the original handler.
Define a handler type as a function:
type Handler func(request string) stringMiddleware functions take a handler and return a new handler with enhanced behavior:
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}Stack multiple middleware by composing them:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// Output: [LOG] [AUTH] Response to: GET /usersThe outermost middleware runs first, determining the execution order. Each middleware can add behavior before or after calling the wrapped handler.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read number of middleware layers
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// Read middleware types
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// Read the request
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: Start with the BaseHandler
// TODO: Build the pipeline by wrapping the handler with middleware
// Apply middleware in order - first middleware read becomes outermost wrapper
// Hint: Loop through middlewareTypes in REVERSE order to achieve this
// The last middleware applied will be the outermost (runs first)
// TODO: Process the request through the composed handler and print the result
fmt.Println("TODO: implement pipeline")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser11Standard Library & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json with Structshttp.Handler InterfaceRecap - REST API Models14Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite PatternMiddleware as Decorator3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool