Go dla początkujących - Część 12: Middleware
Mateusz Kędziora

Hej! Dziś porozmawiamy o koncepcji, która znacząco ułatwia budowanie skalowalnych i dobrze zorganizowanych aplikacji: middleware. Brzmi trochę tajemniczo, ale obiecuję, że po przeczytaniu tego artykułu zrozumiesz, czym jest middleware, jak go używać i dlaczego warto to robić.
Czym właściwie jest middleware?
Wyobraź sobie, że Twoja aplikacja webowa to restauracja. Kiedy klient składa zamówienie (żądanie HTTP), kelner (Twoja funkcja obsługująca żądanie) natychmiast je realizuje. Ale co, jeśli chcesz, żeby każdy klient przeszedł przez szatnię (sprawdzenie autoryzacji), zanim otrzyma swoje danie? Albo co, jeśli chcesz, żeby każdy posiłek był sprawdzany przez szefa kuchni (logowanie żądań) pod kątem jakości, zanim trafi do klienta?
Właśnie w tym miejscu wkracza middleware. Middleware to funkcja, która przechwytuje żądanie HTTP zanim dotrze ono do docelowej funkcji obsługującej żądanie (handler). Może wykonywać różne operacje, takie jak:
- Logowanie: Zapisywanie informacji o każdym żądaniu do logów.
- Autoryzacja: Sprawdzanie, czy użytkownik ma uprawnienia do wykonania danej akcji.
- Obsługa CORS: Dodawanie nagłówków HTTP, które pozwalają na dostęp do zasobów z innych domen.
- Kompresja: Kompresja odpowiedzi HTTP w celu zmniejszenia rozmiaru przesyłanych danych.
- Obsługa błędów: Przechwytywanie błędów i zwracanie odpowiednich odpowiedzi HTTP.
- I wiele więcej!
Middleware działa jak “warstwa pośrednia” między Twoim żądaniem a handlerem, pozwalając na dodawanie funkcjonalności bez konieczności modyfikowania samego handlera.
Dlaczego warto używać middleware?
Używanie middleware ma wiele zalet:
- Separacja odpowiedzialności: Kod odpowiedzialny za logowanie, autoryzację itp. jest oddzielony od logiki biznesowej Twojej aplikacji. To sprawia, że kod jest bardziej czytelny, łatwiejszy w utrzymaniu i testowaniu.
- Kod wielokrotnego użytku: Możesz tworzyć middleware, które można używać w wielu różnych handlerach.
- Łatwość rozbudowy: Możesz łatwo dodawać nowe funkcjonalności do swojej aplikacji, tworząc nowe middleware.
- Modularyzacja: Podzielenie aplikacji na mniejsze, niezależne moduły.
Jak napisać prosty middleware w Go
W Go, middleware jest zazwyczaj implementowany jako funkcja, która przyjmuje http.Handler
jako argument i zwraca nowy http.Handler
. Poniżej znajduje się przykład prostego middleware, który loguje adres IP każdego żądania:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// LoggerMiddleware to funkcja middleware, która loguje adres IP każdego żądania.
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Logujemy adres IP żądania
log.Printf("Request from: %s, path: %s, time: %s", r.RemoteAddr, r.URL.Path, time.Now().Format(time.RFC3339))
// Wywołujemy kolejny handler w łańcuchu
next.ServeHTTP(w, r)
})
}
// HelloHandler to prosty handler, który zwraca "Hello, World!".
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
// Tworzymy handler
helloHandler := http.HandlerFunc(HelloHandler)
// Aplikujemy middleware LoggerMiddleware do helloHandler
loggedHelloHandler := LoggerMiddleware(helloHandler)
// Rejestrujemy handler z middleware
http.Handle("/", loggedHelloHandler)
// Uruchamiamy serwer
log.Println("Server listening on port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Wyjaśnienie kodu:
LoggerMiddleware(next http.Handler) http.Handler
: To jest nasza funkcja middleware. Przyjmujehttp.Handler
(czyli kolejny handler w łańcuchu) jako argument i zwraca nowyhttp.Handler
.http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... })
: Tworzymy anonimową funkcję, która implementuje interfejshttp.Handler
. Ta funkcja będzie wykonywana dla każdego żądania.log.Printf("Request from: %s", r.RemoteAddr)
: Wewnątrz tej funkcji logujemy adres IP żądania, korzystając zr.RemoteAddr
.next.ServeHTTP(w, r)
: To najważniejsza linijka! WywołujemyServeHTTP
na następnym handlerze w łańcuchu. To powoduje, że żądanie jest przekazywane do docelowego handlera (w naszym przypadkuHelloHandler
).
Jak to działa?
Kiedy przeglądarka wysyła żądanie na adres /
, serwer HTTP najpierw wywołuje LoggerMiddleware
. LoggerMiddleware
loguje adres IP żądania, a następnie wywołuje HelloHandler
. HelloHandler
zwraca “Hello, World!”, który jest wyświetlany w przeglądarce.
Uruchomienie przykładu:
- Zapisz kod jako
main.go
. - Otwórz terminal w tym samym katalogu i uruchom:
go run main.go
- Otwórz przeglądarkę i wpisz
http://localhost:8080/
- Sprawdź logi w terminalu. Powinieneś zobaczyć adres IP żądania.
Tworzenie łańcucha middleware
Prawdziwa moc middleware tkwi w możliwości łączenia ich w łańcuchy. Możesz stworzyć sekwencję middleware, gdzie każde middleware wykonuje swoją operację, a następnie przekazuje żądanie do następnego middleware w łańcuchu.
Oto przykład, który łączy LoggerMiddleware
z AuthMiddleware
(middleware do autoryzacji):
package main
import (
"fmt"
"log"
"net/http"
)
// LoggerMiddleware to funkcja middleware, która loguje ścieżkę każdego żądania.
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request path: %s", r.URL.Path)
next.ServeHTTP(w, r)
})
}
// AuthMiddleware to funkcja middleware, która sprawdza, czy użytkownik jest autoryzowany.
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Symulacja sprawdzenia autoryzacji
authorized := true // W prawdziwej aplikacji tutaj sprawdzamy, czy użytkownik jest zalogowany
if authorized {
// Użytkownik jest autoryzowany, przekazujemy żądanie dalej
next.ServeHTTP(w, r)
} else {
// Użytkownik nie jest autoryzowany, zwracamy błąd
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
})
}
// HelloHandler to prosty handler, który zwraca "Hello, World!".
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
// Tworzymy handler
helloHandler := http.HandlerFunc(HelloHandler)
// Tworzymy łańcuch middleware: AuthMiddleware -> LoggerMiddleware -> helloHandler
// Najpierw aplikujemy LoggerMiddleware do helloHandler
loggedHelloHandler := LoggerMiddleware(helloHandler)
// Następnie aplikujemy AuthMiddleware do loggedHelloHandler
authorizedLoggedHelloHandler := AuthMiddleware(loggedHelloHandler)
// Rejestrujemy handler z middleware
http.Handle("/", authorizedLoggedHelloHandler)
// Uruchamiamy serwer
log.Println("Server listening on port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Wyjaśnienie kodu:
AuthMiddleware(next http.Handler) http.Handler
: Funkcja middleware do autoryzacji. W tym przykładzie, dla uproszczenia, zakładamy, że użytkownik jest zawsze autoryzowany (authorized := true
). W prawdziwej aplikacji tutaj sprawdzasz, czy użytkownik jest zalogowany, na podstawie ciasteczek, tokenów JWT itp.if authorized { next.ServeHTTP(w, r) } else { ... }
: Jeśli użytkownik jest autoryzowany, przekazujemy żądanie dalej do następnego handlera w łańcuchu. W przeciwnym razie zwracamy błąd401 Unauthorized
.authorizedLoggedHelloHandler := AuthMiddleware(LoggerMiddleware(helloHandler))
: Tutaj tworzymy łańcuch middleware. Najpierw aplikujemyLoggerMiddleware
dohelloHandler
, a następnie aplikujemyAuthMiddleware
do wyniku. Kolejność jest ważna! Middleware są wykonywane w kolejności, w jakiej są aplikowane.
Jak to działa?
Kiedy przeglądarka wysyła żądanie na adres /
, serwer HTTP najpierw wywołuje AuthMiddleware
. AuthMiddleware
sprawdza, czy użytkownik jest autoryzowany. Jeśli tak, wywołuje LoggerMiddleware
. LoggerMiddleware
loguje ścieżkę żądania, a następnie wywołuje HelloHandler
. HelloHandler
zwraca “Hello, World!”, który jest wyświetlany w przeglądarce. Jeśli użytkownik nie jest autoryzowany, AuthMiddleware
zwraca błąd 401 Unauthorized
i HelloHandler
nie jest wywoływany.
Przykłady użytecznych middleware
Oto kilka przykładów popularnych i użytecznych middleware:
CORS Middleware: Umożliwia dostęp do Twojego API z innych domen (wymagane, jeśli masz frontend i backend na różnych domenach).
func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") // Ustaw na konkretną domenę w produkcji w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) }
Recovery Middleware: Przechwytuje paniki (błędy) w Twojej aplikacji i zwraca ładną odpowiedź HTTP zamiast wywalać cały serwer.
import ( "fmt" "log" "net/http" "runtime/debug" ) // RecoveryMiddleware to funkcja middleware, która obsługuje paniki w aplikacji. func RecoveryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { // Logowanie stack trace (przydatne do debugowania) log.Printf("Panic: %v\n%s", err, debug.Stack()) // Zwracamy informację o błędzie użytkownikowi http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) }
Gzip Middleware: Kompresuje odpowiedzi HTTP, zmniejszając rozmiar przesyłanych danych i przyspieszając ładowanie strony.
import ( "compress/gzip" "io" "log" "net/http" "strings" ) type gzipResponseWriter struct { io.Writer http.ResponseWriter } func (w gzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } // GzipMiddleware to funkcja middleware, która kompresuje odpowiedź HTTP za pomocą gzip. func GzipMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { // Klient nie akceptuje gzip, przekazujemy żądanie dalej bez kompresji next.ServeHTTP(w, r) return } // Ustawiamy nagłówek Content-Encoding w.Header().Set("Content-Encoding", "gzip") // Tworzymy gzip writer gz := gzip.NewWriter(w) defer gz.Close() // Przekazujemy gzip writer do naszego customowego ResponseWriter gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w} // Przekazujemy żądanie do następnego handlera next.ServeHTTP(gzw, r) }) }
Alternatywne sposoby łączenia middleware
Istnieją pakiety, które upraszczają proces łączenia middleware. Jednym z popularniejszych jest github.com/gorilla/mux
. Omawiany przy okazji poprzedniego artykułu.
//Przykładowy kod z pakietu mux
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
// middlewareHandler to funkcja, która obsługuje middleware
type middlewareHandler func(http.Handler) http.Handler
// chainMiddleware łańcuchy middleware
func chainMiddleware(handler http.Handler, middlewares ...middlewareHandler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// simpleMiddleware to prosty middleware
func simpleMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Before handler")
next.ServeHTTP(w, r)
log.Println("After handler")
})
}
// HelloHandler to prosty handler
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
r := mux.NewRouter()
// Użycie łańcucha middleware
wrappedHandler := chainMiddleware(http.HandlerFunc(HelloHandler), simpleMiddleware, simpleMiddleware)
r.Handle("/", wrappedHandler)
// Uruchomienie serwera
log.Println("Server listening on port 8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
Praca domowa
- Stwórz middleware, który dodaje nagłówek
X-Powered-By: Go
do każdej odpowiedzi HTTP. - Stwórz middleware, które mierzy czas trwania każdego żądania i loguje go.
- Spróbuj zaimplementować własną wersję CORS middleware, która pozwala tylko na dostęp z określonej domeny. (Skup się na podstawowej funkcjonalności, nie musisz obsługiwać wszystkich opcji CORS).
- Połącz wszystkie stworzone middleware w łańcuch i przetestuj je.
Podsumowanie
Middleware to potężne narzędzie, które pozwala na dodawanie funkcjonalności do Twojej aplikacji bez konieczności modyfikowania samego kodu obsługi żądań. Dzięki middleware Twój kod staje się bardziej czytelny, łatwiejszy w utrzymaniu i testowaniu. Mam nadzieję, że ten artykuł pomógł Ci zrozumieć, czym jest middleware i jak go używać w Go.
Zachęcam do eksperymentowania z różnymi middleware i tworzenia własnych. Pamiętaj, że im więcej praktyki, tym lepiej zrozumiesz tę koncepcję.
Przydatne zasoby
- Oficjalna dokumentacja języka Go: https://go.dev/
- Dokumentacja pakietu
net/http
: https://pkg.go.dev/net/http - A Tour of Go: https://go.dev/tour/welcome/1
- Effective Go: https://go.dev/doc/effective_go
- Go by Example: https://gobyexample.com/
- Gorilla Mux: https://github.com/gorilla/mux
Nie zapomnij sprawdzić pozostałych artykułów na blogu, gdzie omawiamy inne aspekty języka Go i web developmentu. Powodzenia w dalszej nauce!
Polecane artykuły
Docker vs Kubernetes: Który dla Ciebie w 2025?
Docker i Kubernetes objaśnione! Która technologia lepsza dla początkujących w 2025? Porównanie, przykłady i przyszłość.
Mateusz Kędziora
DevOps: Automatyzacja zadań sysadmina dla programistów
Zautomatyzuj pracę sysadmina w środowisku DevOps! Praktyczne przykłady, skrypty, Ansible, Terraform, Prometheus i Grafana.
Mateusz Kędziora
Automatyzacja Linux/macOS z Bash: Praktyczny Przewodnik
Zacznij automatyzować system Linux/macOS z Bash! Dowiedz się, czym jest Bash, jak pisać skrypty i używać podstawowych komend.
Mateusz Kędziora