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

2/10/2025 Kurs Go

Mateusz Kędziora

image

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:

  1. LoggerMiddleware(next http.Handler) http.Handler: To jest nasza funkcja middleware. Przyjmuje http.Handler (czyli kolejny handler w łańcuchu) jako argument i zwraca nowy http.Handler.
  2. http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... }): Tworzymy anonimową funkcję, która implementuje interfejs http.Handler. Ta funkcja będzie wykonywana dla każdego żądania.
  3. log.Printf("Request from: %s", r.RemoteAddr): Wewnątrz tej funkcji logujemy adres IP żądania, korzystając z r.RemoteAddr.
  4. next.ServeHTTP(w, r): To najważniejsza linijka! Wywołujemy ServeHTTP na następnym handlerze w łańcuchu. To powoduje, że żądanie jest przekazywane do docelowego handlera (w naszym przypadku HelloHandler).

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:

  1. Zapisz kod jako main.go.
  2. Otwórz terminal w tym samym katalogu i uruchom: go run main.go
  3. Otwórz przeglądarkę i wpisz http://localhost:8080/
  4. 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:

  1. 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.
  2. 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łąd 401 Unauthorized.
  3. authorizedLoggedHelloHandler := AuthMiddleware(LoggerMiddleware(helloHandler)): Tutaj tworzymy łańcuch middleware. Najpierw aplikujemy LoggerMiddleware do helloHandler, a następnie aplikujemy AuthMiddleware 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

  1. Stwórz middleware, który dodaje nagłówek X-Powered-By: Go do każdej odpowiedzi HTTP.
  2. Stwórz middleware, które mierzy czas trwania każdego żądania i loguje go.
  3. 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).
  4. 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

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