Middleware на уровне сетевого стэка в Go
- понедельник, 18 ноября 2024 г. в 00:00:07
Привет, любители Go! Сегодня мы рассмотрим, как создать middleware на уровне сетевого стэка в Go. Middleware позволяет добавлять полезные функции к HTTP-запросам и ответам: логирование, аутентификация, обработка ошибок и многое другое.
Начнем с классики – 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
будет логироваться.
Пора подняться на следующий уровень и поработать с транспортным уровнем. Здесь будем использовать интерфейс 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, когда можно создать целую цепочку? Давайте создадим функцию 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".
А все лучшие практики, инструменты и подходы к построению архитектуры приложений можно изучить на практических курсах. Подробности в каталоге.