Middleware als Decorator
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 97 von 107.
Middleware ist eine der praktischsten Anwendungen des Decorator-Musters in Go. In der Webentwicklung umschließen Middleware-Funktionen HTTP-Handler, um querschnittliche Belange wie Logging, Authentifizierung oder Zeitmessung hinzuzufügen, ohne den ursprünglichen Handler zu verändern.
Die entscheidende Erkenntnis ist, dass Middleware einen Handler entgegennimmt und einen neuen Handler mit zusätzlichem Verhalten zurückgibt. Dies folgt exakt dem Decorator-Muster – dieselbe Schnittstelle am Eingang und Ausgang, mit erweiterter Funktionalität:
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)
}
}Jede Middleware umschließt den nächsten Handler in der Kette und fügt ihr Verhalten vor oder nach dem Aufruf des umschlossenen Handlers hinzu. Sie können mehrere Middlewares stapeln, indem Sie diese kombinieren:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Antwort auf: GET /usersDie Reihenfolge des Wrappings bestimmt die Ausführungsreihenfolge – die äußerste Middleware wird zuerst ausgeführt. Dieses Muster wird in Go-Web-Frameworks ausgiebig für Request-Processing-Pipelines verwendet, was es einfach macht, Funktionalität hinzuzufügen oder zu entfernen, ohne die Kernlogik des Handlers zu berühren.
Aufgabe
EinfachLassen wir uns eine Anfrageverarbeitungspipeline mit dem Middleware-als-Decorator-Muster bauen! Sie erstellen ein System, in dem Middleware-Funktionen Handler umschließen, um Funktionen wie Zeitmessung, Validierung und Formatierung hinzuzufügen – und das alles, ohne die Kernlogik des Handlers zu ändern.
Sie organisieren Ihren Code in drei Dateien:
handler.go: Definieren Sie Ihren Handler-Typ und den Basis-Handler, der Anfragen verarbeitet.Erstellen Sie einen
Handler-Typ als Funktion, die einen Request-String entgegennimmt und einen Response-String zurückgibt:type Handler func(request string) stringImplementieren Sie eine
BaseHandler-Funktion, dieProcessed: [request]für jede empfangene Anfrage zurückgibt.middleware.go: Erstellen Sie Ihre Middleware-Funktionen, die Handler mit zusätzlichem Verhalten umschließen.Erstellen Sie drei Middleware-Funktionen, die jeweils einen
Handlerentgegennehmen und einen neuenHandlerzurückgeben:TimingMiddleware— umschließt die Antwort mit Zeitmessungsinformationen und gibt[TIMING] [response]zurückValidationMiddleware— fügt ein Validierungspräfix hinzu und gibt[VALID] [response]zurückUppercaseMiddleware— wandelt die gesamte Antwort in Großbuchstaben um
Jede Middleware sollte den umschlossenen Handler aufrufen und dessen Ergebnis erweitern.
main.go: Bauen Sie eine Verarbeitungspipeline durch Komposition von Middleware auf.Lesen Sie die Anzahl der anzuwendenden Middleware-Schichten ein. Lesen Sie dann für jede Schicht den Middleware-Typ (
timing,validationoderuppercase) ein und umschließen Sie Ihren aktuellen Handler mit dieser Middleware. Wenden Sie diese in der Reihenfolge an, in der sie gelesen werden – die zuerst gelesene Middleware wird zur äußersten Hülle.Nachdem Sie die Pipeline aufgebaut haben, lesen Sie den Request-String ein, verarbeiten ihn durch Ihren zusammengesetzten Handler und geben das Ergebnis aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anzahl der Middleware-Schichten
- Folgende Zeilen: Ein Middleware-Typ pro Zeile (
timing,validationoderuppercase) - Letzte Zeile: Die zu verarbeitende Anfrage
Zum Beispiel, gegeben:
2
timing
validation
GET /usersSollte Ihre Ausgabe wie folgt aussehen:
[TIMING] [VALID] Processed: GET /usersUnd gegeben:
3
uppercase
timing
validation
POST /dataSollte Ihre Ausgabe wie folgt aussehen:
[TIMING] [VALID] PROCESSED: POST /DATAUnd gegeben:
1
uppercase
hello worldSollte Ihre Ausgabe wie folgt aussehen:
PROCESSED: HELLO WORLDUnd gegeben:
0
simple requestSollte Ihre Ausgabe wie folgt aussehen:
Processed: simple requestBeachten Sie, wie die Reihenfolge der Middleware-Umschließung die Ausgabe beeinflusst – die zuerst angewendete Middleware (die äußerste) wird zuerst ausgeführt und sieht das Endergebnis aller inneren Middlewares. Die Uppercase-Middleware im zweiten Beispiel wird zuletzt (als innerste) ausgeführt, sodass sie die Basisantwort in Großbuchstaben umwandelt, bevor Timing und Validation ihre Präfixe hinzufügen!
Spickzettel
Middleware in Go folgt dem Decorator-Muster, indem Handler eingepackt werden, um Funktionalität hinzuzufügen, ohne den ursprünglichen Handler zu verändern.
Definieren Sie einen Handler-Typ als Funktion:
type Handler func(request string) stringMiddleware-Funktionen nehmen einen Handler entgegen und geben einen neuen Handler mit erweitertem Verhalten zurück:
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)
}
}Stapeln Sie mehrere Middlewares, indem Sie sie zusammensetzen:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// Ausgabe: [LOG] [AUTH] Response to: GET /usersDie äußerste Middleware wird zuerst ausgeführt und bestimmt die Reihenfolge der Ausführung. Jede Middleware kann Verhalten vor oder nach dem Aufruf des eingepackten Handlers hinzufügen.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Anzahl der Middleware-Schichten lesen
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// Middleware-Typen lesen
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// Die Anfrage lesen
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: Mit dem BaseHandler beginnen
// TODO: Die Pipeline aufbauen, indem der Handler mit Middleware umschlossen wird
// Middleware in der richtigen Reihenfolge anwenden - die zuerst gelesene Middleware wird zur äußersten Hülle
// Hinweis: Durchlaufe middlewareTypes in UMGEKEHRTER Reihenfolge, um dies zu erreichen
// Die zuletzt angewendete Middleware wird die äußerste sein (wird zuerst ausgeführt)
// TODO: Die Anfrage durch den zusammengesetzten Handler verarbeiten und das Ergebnis ausgeben
fmt.Println("TODO: implement pipeline")
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der Go OOP
Externe DateienGo Workspace & ModulePackages & ImportsExportierte vs. nicht-exportierte NamenEinführung in OOP in GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktor-FunktionenZusammenfassung – Einfacher Taschenrechner4Interfaces
Einführung in InterfacesImplizite ImplementierungInterface als VertragLeeres Interface (any)Type AssertionType SwitchInterface-KompositionStringer & Error InterfacesRückblick - Formen-Rechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf Paket-EbeneGetter- & Setter-MethodenInformation Hiding in GoZusammenfassung - Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterType ConstraintsGenerische StructsWorkaround für generische MethodenZusammenfassung – Generische Collection2Vertiefung: Typen & Structs
Basis- & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct TagsAnonyme StructsVerschachtelte StructsZero Values & StandardwerteZusammenfassung - Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen des Struct EmbeddingMethod PromotionEinbetten mehrerer StructsEmbedding vs. AggregationShadowing eingebetteter MethodenZusammenfassung – Mitarbeiter-Hierarchie8Fehlerbehandlung & OOP
Das error-InterfaceEigene FehlertypenError Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverZusammenfassung – Datei-Parser11Standardbibliothek & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json mit Structshttp.Handler InterfaceZusammenfassung - REST-API-Modelle14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterMiddleware als Decorator3Zeiger & Speicher
Zeiger-Grundlagen in GoZeiger auf StructsWertübergabe vs. ReferenzübergabeDie new() FunktionGarbage Collection in GoWiederholung – Linked List Builder6Polymorphismus in Go
Polymorphismus über InterfacesDuck Typing in GoInterface-ErfüllungsregelnPolymorphe CollectionsDependency InjectionZusammenfassung – Payment Processor9Nebenläufigkeit & OOP
Goroutines GrundlagenChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignZusammenfassung - Worker Pool