Привет, любители Go! Сегодня мы рассмотрим, как создать middleware на уровне сетевого стэка в Go. Middleware позволяет добавлять полезные функции к HTTP-запросам и ответам: логирование, аутентификация, обработка ошибок и многое другое.
Простой пример Middleware
Начнем с классики – middleware для логирования запросов:
package main import ( "log" "net/http" "time" ) // loggingMiddleware логирует начало и конец обработки запроса. func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() log.Printf("🚀 Старт обработки %s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) log.Printf("🏁 Завершено за %v", time.Since(start)) }) } // helloHandler – простой обработчик, который приветствует мир. func helloHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Привет, мир! 🌍")) } func main() { mux := http.NewServeMux() mux.HandleFunc("/hello", helloHandler) // Оборачиваем mux в middleware loggedMux := loggingMiddleware(mux) log.Println("🛡️ Сервер запущен на :8080") if err := http.ListenAndServe(":8080", loggedMux); err != nil { log.Fatalf("❌ Ошибка запуска сервера: %v", err) } }
loggingMiddleware: принимает http.Handler
, оборачивает его и добавляет логирование до и после обработки запроса.
helloHandler: просто отвечает строкой «Привет, мир! 🌍».
main: создаем ServeMux
, регистрируем обработчик, оборачиваем его в loggingMiddleware
и запускаем сервер.
Чтобы проверить, запустим сервер:
go run main.go
Затем переходим в браузере по адресу http://localhost:8080/hello. В терминале вы увидите что-то вроде:
🚀 Старт обработки GET /hello 🏁 Завершено за 150µs
Супер. Теперь каждый запрос к /hello
будет логироваться.
Кастомные Middleware на уровне транспорта
Пора подняться на следующий уровень и поработать с транспортным уровнем. Здесь будем использовать интерфейс http.RoundTripper
, который позволяет вмешиваться в процесс отправки и получения HTTP-запросов.
Создадим middleware, который добавляет кастомный заголовок ко всем исходящим запросам.
package main import ( "log" "net/http" ) // CustomTransport – кастомный транспорт, добавляющий заголовок type CustomTransport struct { Transport http.RoundTripper } // RoundTrip – метод, который добавляет заголовок и выполняет запрос func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { // Клонируем запрос, чтобы не мутировать оригинал clonedReq := req.Clone(req.Context()) clonedReq.Header.Set("X-Custom-Header", "GoMiddleware") log.Printf("🛠️ Добавлен заголовок X-Custom-Header для %s %s", clonedReq.Method, clonedReq.URL) return t.Transport.RoundTrip(clonedReq) } func main() { client := &http.Client{ Transport: &CustomTransport{ Transport: http.DefaultTransport, }, } resp, err := client.Get("https://httpbin.org/get") if err != nil { log.Fatalf("❌ Ошибка выполнения запроса: %v", err) } defer resp.Body.Close() log.Printf("✅ Статус ответа: %s", resp.Status) }
CustomTransport: реализует интерфейс http.RoundTripper
и добавляет заголовок X-Custom-Header
ко всем запросам.
RoundTrip: клонирует запрос, добавляет заголовок и выполняет запрос через базовый транспорт.
main: создает HTTP-клиент с нашим кастомным транспортом и выполняет GET-запрос.
Запускаем клиентский код и проверяем логи:
🛠️ Добавлен заголовок X-Custom-Header для GET https://httpbin.org/get ✅ Статус ответа: 200 OK
Если зайти на http://httpbin.org/get, вы увидите, что заголовок действительно добавлен.
Комбинируем Middleware
Почему ограничиваться одним middleware, когда можно создать целую цепочку? Давайте создадим функцию ChainRoundTripper
, которая позволит комбинировать несколько middleware.
Функция ChainRoundTripper:
// ChainRoundTripper – функция для объединения нескольких RoundTripper func ChainRoundTripper(rt http.RoundTripper, middlewares ...func(http.RoundTripper) http.RoundTripper) http.RoundTripper { for _, m := range middlewares { rt = m(rt) } return rt }
Создадим еще одно middleware для логирования запросов и объединим его с нашим CustomTransport
.
package main import ( "log" "net/http" ) // LoggingTransport – логирует каждый запрос type LoggingTransport struct { Transport http.RoundTripper } func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { log.Printf("🔍 Запрос: %s %s", req.Method, req.URL) return t.Transport.RoundTrip(req) } func main() { client := &http.Client{ Transport: ChainRoundTripper(http.DefaultTransport, func(rt http.RoundTripper) http.RoundTripper { return &CustomTransport{Transport: rt} }, func(rt http.RoundTripper) http.RoundTripper { return &LoggingTransport{Transport: rt} }, ), } resp, err := client.Get("https://httpbin.org/get") if err != nil { log.Fatalf("❌ Ошибка выполнения запроса: %v", err) } defer resp.Body.Close() log.Printf("✅ Статус ответа: %s", resp.Status) }
LoggingTransport: логирует каждый запрос перед его выполнением.
main: использует ChainRoundTripper
для объединения CustomTransport
и LoggingTransport
. Теперь каждый запрос будет логироваться и иметь добавленный заголовок.
Результат:
🔍 Запрос: GET https://httpbin.org/get 🛠️ Добавлен заголовок X-Custom-Header для GET https://httpbin.org/get ✅ Статус ответа: 200 OK
Отлично! Теперь есть мощная цепочка middleware, которая делает HTTP-клиент еще круче.
Оптимизация
Middleware – это здорово, но не будем забывать про производительность.
sync.Pool
позволяет переиспользовать объекты, снижая нагрузку на сборщик мусора.
package main import ( "log" "net/http" "sync" ) // requestPool – пул для повторного использования объектов http.Request var requestPool = sync.Pool{ New: func() interface{} { return new(http.Request) }, } // CustomTransport с использованием пула type CustomTransport struct { Transport http.RoundTripper } func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { // Получаем объект из пула pooledReq := requestPool.Get().(*http.Request) *pooledReq = *req // Копируем данные pooledReq.Header.Set("X-Custom-Header", "GoMiddleware") resp, err := t.Transport.RoundTrip(pooledReq) // Возвращаем объект в пул requestPool.Put(pooledReq) return resp, err } func main() { client := &http.Client{ Transport: &CustomTransport{ Transport: http.DefaultTransport, }, } resp, err := client.Get("https://httpbin.org/get") if err != nil { log.Fatalf("❌ Ошибка выполнения запроса: %v", err) } defer resp.Body.Close() log.Printf("✅ Статус ответа: %s", resp.Status) }
Обработка протоколов на низком уровне
Иногда хочется погрузиться глубже и управлять соединениями на уровне TCP. Создадим простой TCP-сервер, который читает данные, модифицирует их и отправляет обратно.
Простой TCP-Сервер:
package main import ( "bufio" "io" "log" "net" "strings" ) // handleConnection – обрабатывает каждое подключение func handleConnection(conn net.Conn) { defer conn.Close() log.Printf("🔗 Новое соединение с %s", conn.RemoteAddr()) reader := bufio.NewReader(conn) for { data, err := reader.ReadString('\n') if err != nil { if err != io.EOF { log.Printf("❌ Ошибка чтения данных: %v", err) } break } data = strings.TrimSpace(data) log.Printf("📥 Получено: %s", data) // Модифицируем данные: делаем их заглавными modifiedData := strings.ToUpper(data) + "\n" _, err = conn.Write([]byte("💬 Echo: " + modifiedData)) if err != nil { log.Printf("❌ Ошибка отправки данных: %v", err) break } } } func main() { ln, err := net.Listen("tcp", ":8081") if err != nil { log.Fatalf("❌ Ошибка запуска TCP-сервера: %v", err) } defer ln.Close() log.Println("🔧 TCP-сервер запущен на :8081") for { conn, err := ln.Accept() if err != nil { log.Printf("❌ Ошибка принятия соединения: %v", err) continue } go handleConnection(conn) } }
handleConnection принимает соединение, читает данные построчно, превращает их в верхний регистр и отправляет обратно, а main запускает TCP-сервер на порту 8081
и обрабатывает каждое соединение в отдельной горутине.
Запускаем сервер и в другом терминале используйте telnet
или nc
для подключения:
telnet localhost 8081
Вводим строку, например, hello
, и получите ответ Echo: HELLO
.
Безопасность
Безопасность – это не шутки. Добавим немного защиты в middleware, чтобы никто не смог подставить свои данные.
Создадим middleware, которое проверяет наличие и корректность токена авторизации.
package main import ( "log" "net/http" ) // authMiddleware – проверяет заголовок Authorization func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "Bearer supersecrettoken" { log.Printf("🚫 Неавторизованный доступ к %s %s", r.Method, r.URL.Path) http.Error(w, "Forbidden", http.StatusForbidden) return } // Продолжаем обработку запроса next.ServeHTTP(w, r) }) } // loggingMiddleware – уже знакомый middleware для логирования func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("🚀 Старт обработки %s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) log.Printf("🏁 Завершено за %v", time.Since(start)) }) } // secureHandler – защищенный обработчик func secureHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("🔒 Secure Content")) } func main() { mux := http.NewServeMux() mux.HandleFunc("/secure", secureHandler) // Комбинируем middleware: сначала логирование, потом аутентификация handler := authMiddleware(loggingMiddleware(mux)) log.Println("🛡️ Сервер с аутентификацией запущен на :8080") if err := http.ListenAndServe(":8080", handler); err != nil { log.Fatalf("❌ Ошибка запуска сервера: %v", err) } }
Попробуем выполнить запросы с и без правильного токена:
# Без токена curl -i http://localhost:8080/secure # Ответ: 403 Forbidden # С неверным токеном curl -i -H "Authorization: Bearer wrongtoken" http://localhost:8080/secure # Ответ: 403 Forbidden # С правильным токеном curl -i -H "Authorization: Bearer supersecrettoken" http://localhost:8080/secure # Ответ: 200 OK # Тело ответа: 🔒 Secure Content
Результаты:
🚫 Неавторизованный доступ к GET /secure
При правильном токене:
🚀 Старт обработки GET /secure 🏁 Завершено за 200µs
Полный пример
Теперь соберем всё вместе и создадим полноценный HTTP-сервер с несколькими middleware.
package main import ( "log" "net/http" "sync" "time" ) // loggingMiddleware – логирует HTTP-запросы func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() log.Printf("🚀 Старт обработки %s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) log.Printf("🏁 Завершено за %v", time.Since(start)) }) } // authMiddleware – проверяет заголовок Authorization func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "Bearer supersecrettoken" { log.Printf("🚫 Неавторизованный доступ к %s %s", r.Method, r.URL.Path) http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // CustomTransport – добавляет кастомный заголовок к исходящим запросам type CustomTransport struct { Transport http.RoundTripper Pool *sync.Pool } func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { // Получаем объект из пула pooledReq := t.Pool.Get().(*http.Request) *pooledReq = *req // Копируем данные pooledReq.Header.Set("X-Custom-Header", "GoMiddleware") log.Printf("🛠️ Добавлен заголовок X-Custom-Header для %s %s", pooledReq.Method, pooledReq.URL) resp, err := t.Transport.RoundTrip(pooledReq) // Возвращаем объект в пул t.Pool.Put(pooledReq) return resp, err } // secureHandler – защищенный обработчик func secureHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("🔒 Secure Content")) } func main() { mux := http.NewServeMux() mux.HandleFunc("/secure", secureHandler) // Комбинируем middleware: сначала логирование, потом аутентификация handler := authMiddleware(loggingMiddleware(mux)) // Создаем пул для http.Request requestPool := &sync.Pool{ New: func() interface{} { return new(http.Request) }, } // Создаем HTTP-клиента с кастомным транспортом client := &http.Client{ Transport: &CustomTransport{ Transport: http.DefaultTransport, Pool: requestPool, }, } // Пример использования клиента go func() { time.Sleep(2 * time.Second) // Ждем, пока сервер запустится req, _ := http.NewRequest("GET", "https://httpbin.org/get", nil) resp, err := client.Do(req) if err != nil { log.Printf("❌ Ошибка выполнения запроса: %v", err) return } resp.Body.Close() log.Printf("✅ Клиент получил ответ: %s", resp.Status) }() log.Println("🛡️ Сервер с несколькими middleware запущен на :8080") if err := http.ListenAndServe(":8080", handler); err != nil { log.Fatalf("❌ Ошибка запуска сервера: %v", err) } }
Заключение
Надеюсь, эта статья принесла вам пользу. Если у вас есть вопросы, идеи или вы хотите поделиться своими наработками, пишите в комментариях. Всегда рад обсудить и помочь!
19 ноября в Otus пройдет урок на тему «Паттерны отказоустойчивости и масштабируемости микросервисной архитектуры», записаться на него можно на странице курса «Software Architect».
А все лучшие практики, инструменты и подходы к построению архитектуры приложений можно изучить на практических курсах. Подробности в каталоге.
ссылка на оригинал статьи https://habr.com/ru/articles/857070/
Добавить комментарий