HTTP клиент на Go: продвинутые техники работы с запросами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

HTTP клиент на Go: делаем запросы правильно (продвинутый уровень)

Встроенный пакет net/http в Go предоставляет мощные, но часто недооцененные возможности для создания надежных HTTP-клиентов. На начальном уровне разработчики используют http.Get или http.Post, что удобно для быстрых прототипов, но совершенно непригодно для продакшена. В этой статье мы разберем продвинутые техники настройки HTTP-клиента на Go: тонкая настройка таймаутов, управление пулом соединений, создание middleware-цепочек, реализация ретраев и грамотная обработка ошибок. Вы научитесь писать клиентский код, который выдерживает высокие нагрузки и устойчив к сетевым сбоям.

1. Настройка таймаутов и пула соединений

Базовая ошибка — использование http.DefaultClient. У него нет таймаутов, и он может висеть бесконечно. Для продакшена всегда создавайте собственный экземпляр http.Client с тонкой настройкой http.Transport.

package main

import (
    "net/http"
    "time"
)

func NewClient() *http.Client {
    transport := &http.Transport{
        MaxIdleConns:        100,               // Максимум idle-соединений во всем пуле
        MaxConnsPerHost:     10,                // Максимум соединений к одному хосту
        MaxIdleConnsPerHost: 5,                 // Максимум idle-соединений к одному хосту
        IdleConnTimeout:     90 * time.Second,  // Время жизни idle-соединения
        DisableCompression:  false,             // Включаем сжатие (gzip)
    }

    client := &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second, // Общий таймаут на весь запрос
    }
    return client
}

Разберем ключевые параметры:

  • Timeout — общий лимит времени на выполнение запроса (включая чтение тела ответа). Устанавливайте его в зависимости от ожидаемой скорости API (обычно 10-30 секунд).
  • MaxIdleConns — количество keep-alive соединений, которые хранятся в пуле. Для высоконагруженных систем увеличьте до 200-500.
  • MaxConnsPerHost — ограничивает количество одновременных соединений к одному хосту. Предотвращает перегрузку сервера.

Также рекомендуется установить DialContext с собственным таймаутом на установку соединения:

transport.DialContext = (&net.Dialer{
    Timeout:   5 * time.Second,  // Таймаут на TCP handshake
    KeepAlive: 30 * time.Second, // Keep-alive для поддержания соединения
}).DialContext

2. Создание middleware-цепочек для HTTP-клиента

В Go нет встроенной middleware для клиента, но мы можем легко реализовать паттерн "декоратор" через обертку http.RoundTripper. Это позволяет добавлять логирование, метрики, аутентификацию и другие сквозные функции.

type LoggingRoundTripper struct {
    Proxied http.RoundTripper
}

func (l *LoggingRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
    start := time.Now()
    resp, err := l.Proxied.RoundTrip(r)
    duration := time.Since(start)

    log.Printf("[%s] %s %s - %d (%v)",
        r.Method, r.URL.String(),
        r.Host, resp.StatusCode, duration,
    )
    return resp, err
}

// Использование:
transport := &LoggingRoundTripper{
    Proxied: http.DefaultTransport,
}
client := &http.Client{Transport: transport}

Можно создать цепочку middleware, например, для добавления заголовков аутентификации:

type AuthRoundTripper struct {
    Proxied http.RoundTripper
    Token   string
}

func (a *AuthRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
    r.Header.Set("Authorization", "Bearer "+a.Token)
    return a.Proxied.RoundTrip(r)
}

// Цепочка: Auth → Logging → Transport
client := &http.Client{
    Transport: &LoggingRoundTripper{
        Proxied: &AuthRoundTripper{
            Proxied: http.DefaultTransport,
            Token:   os.Getenv("API_TOKEN"),
        },
    },
}

3. Реализация ретраев и обработка ошибок

Сетевые сбои неизбежны. Продвинутый клиент должен автоматически повторять запросы при временных ошибках. Важно отличать временные ошибки (таймауты, 5xx) от постоянных (4xx, неверный URL).

func DoWithRetry(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
    var resp *http.Response
    var err error

    for i := 0; i <= maxRetries; i++ {
        // Создаем новый body для каждого ретрая (тело запроса может быть прочитано)
        if req.Body != nil {
            req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
        }

        resp, err = client.Do(req)
        if err != nil {
            // Проверяем, временная ли ошибка
            if isTemporaryError(err) {
                waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
                log.Printf("Retry %d after %v: %v", i+1, waitTime, err)
                time.Sleep(waitTime)
                continue
            }
            return nil, err // Постоянная ошибка, не ретраим
        }

        // Проверяем статус-код
        if resp.StatusCode >= 500 {
            resp.Body.Close()
            if i == maxRetries {
                return nil, fmt.Errorf("server error after %d retries: %d", maxRetries, resp.StatusCode)
            }
            waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
            log.Printf("Retry %d after %v (status %d)", i+1, waitTime, resp.StatusCode)
            time.Sleep(waitTime)
            continue
        }

        return resp, nil // Успех
    }
    return nil, fmt.Errorf("unexpected retry exit")
}

func isTemporaryError(err error) bool {
    netErr, ok := err.(net.Error)
    return ok && netErr.Temporary()
}

Ключевые моменты:

  • Экспоненциальная задержка (backoff): 1с, 2с, 4с и т.д. Это снижает нагрузку на сервер.
  • Проверка временных ошибок: используйте net.Error.Temporary().
  • Сброс тела запроса: для ретраев нужно

Блоги

Рекомендаций по книгам