Привет, любители 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".
А все лучшие практики, инструменты и подходы к построению архитектуры приложений можно изучить на практических курсах. Подробности в каталоге.
          
 
gudvinr
А конкретно в этом примере как sync.Pool помогает?
В RoundTrip уже пришел указатель на Request, зачем нужен ещё один?