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 übergreifende Belange wie Protokollierung, Authentifizierung oder Zeitmessung hinzuzufügen, ohne den ursprünglichen Handler zu ändern.
Die zentrale Erkenntnis ist, dass Middleware einen Handler übernimmt und einen neuen Handler mit zusätzlichem Verhalten zurückgibt. Dies entspricht genau dem Decorator-Muster: dieselbe Schnittstelle hinein und hinaus, 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. Du kannst mehrere Middleware kombinieren, indem du sie zusammensetzt:
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 der Umhüllung bestimmt die Ausführungsreihenfolge: Die äußerste Middleware wird zuerst ausgeführt. Dieses Muster wird in Go-Webframeworks häufig für Pipelines zur Anfrageverarbeitung verwendet und ermöglicht das einfache Hinzufügen oder Entfernen von Funktionalität, ohne die Kernlogik des Handlers zu verändern.
Aufgabe
EinfachErstellen wir eine Anfrageverarbeitungspipeline mit dem Middleware-als-Decorator-Muster! Du erstellst ein System, in dem Middleware-Funktionen Handler umschließen, um Funktionalität wie Zeitmessung, Validierung und Formatierung hinzuzufügen – und das alles, ohne die Kernlogik des Handlers zu verändern.
Du organisierst deinen Code auf drei Dateien verteilt:
handler.go: Definiere deinen Handler-Typ und den Basis-Handler, der Anfragen verarbeitet.Erstelle einen
Handler-Typ als Funktion, die einen Anfrage-String entgegennimmt und einen Antwort-String zurückgibt:type Handler func(request string) stringImplementiere eine
BaseHandler-Funktion, die für jede empfangene AnfrageProcessed: [request]zurückgibt.middleware.go: Erstelle deine Middleware-Funktionen, die Handler mit zusätzlichem Verhalten umschließen.Erstelle drei Middleware-Funktionen, die jeweils einen
Handlerentgegennimmt und einen neuenHandlerzurückgibt: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: Erstelle eine Verarbeitungspipeline, indem du Middleware kombinierst.Lies die Anzahl der anzuwendenden Middleware-Ebenen ein. Lies anschließend für jede Ebene den Middleware-Typ (
timing,validationoderuppercase) ein und umschließe deinen aktuellen Handler mit dieser Middleware. Wende sie in der Reihenfolge an, in der sie eingelesen werden: Die zuerst eingelesene Middleware wird zum äußersten Wrapper.Lies nach dem Erstellen der Pipeline den Anfrage-String ein, verarbeite ihn durch deinen kombinierten Handler und gib das Ergebnis aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anzahl der Middleware-Ebenen
- Folgende Zeilen: Je eine Middleware-Art pro Zeile (
timing,validationoderuppercase) - Letzte Zeile: Die zu verarbeitende Anfrage
Beispielsweise bei:
2
timing
validation
GET /usersDie Ausgabe sollte sein:
[TIMING] [VALID] Processed: GET /usersUnd bei:
3
uppercase
timing
validation
POST /dataDie Ausgabe sollte sein:
[TIMING] [VALID] PROCESSED: POST /DATAUnd bei:
1
uppercase
hello worldDie Ausgabe sollte sein:
PROCESSED: HELLO WORLDUnd bei:
0
simple requestDie Ausgabe sollte sein:
Processed: simple requestBeachte, wie sich die Reihenfolge der Middleware-Umschachtelung auf die Ausgabe auswirkt: Die zuerst angewendete Middleware (die äußerste) wird zuerst ausgeführt und erhält das endgültige Ergebnis aller inneren Middleware-Funktionen. Die Uppercase-Middleware im zweiten Beispiel wird zuletzt ausgeführt (als innerste Middleware), sodass sie die Basisantwort in Großbuchstaben umwandelt, bevor Timing und Validierung ihre Präfixe 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: Beginne mit dem BaseHandler
// TODO: Baue die Pipeline auf, indem du den Handler mit Middleware umhüllst
// Middleware der Reihe nach anwenden – die zuerst gelesene Middleware wird zum äußersten Wrapper
// Hinweis: Gehe middlewareTypes in UMGEKEHRTER Reihenfolge durch, um dies zu erreichen
// Die zuletzt angewendete Middleware wird die äußerste sein (läuft zuerst)
// TODO: Verarbeite die Anfrage durch den zusammengesetzten Handler und gib das Ergebnis aus
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 OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser11Standardbibliothek & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer-Interfaceencoding/json mit Structshttp.Handler-InterfaceRückblick – REST-API-Modelle14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Methoden-MusterState-MusterComposite-MusterMiddleware als Decorator3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler