HTTP клиент на Go: делаем запросы правильно от А до Я

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

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

Начать курс

HTTP клиент на Go: делаем запросы правильно от А до Я

Go (Golang) — это язык, который славится своей простотой и производительностью, особенно в области сетевого программирования. Встроенный пакет net/http предоставляет мощный и гибкий HTTP-клиент, который подходит как для простых запросов, так и для сложных высоконагруженных систем. В этой статье мы разберем, как правильно использовать HTTP-клиент в Go, начиная от базовых запросов и заканчивая продвинутыми настройками.

1. Основы: создание и выполнение простых запросов

Самый простой способ сделать HTTP-запрос в Go — использовать функции-хелперы из пакета net/http, такие как http.Get и http.Post. Однако для продакшн-кода рекомендуется использовать явное создание клиента и запроса.

Базовый GET-запрос:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    // Создаем нового клиента
    client := &http.Client{}

    // Создаем запрос
    req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
    if err != nil {
        fmt.Println("Ошибка создания запроса:", err)
        return
    }

    // Выполняем запрос
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Ошибка выполнения запроса:", err)
        return
    }
    defer resp.Body.Close()

    // Читаем тело ответа
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Ошибка чтения ответа:", err)
        return
    }

    fmt.Println("Статус:", resp.Status)
    fmt.Println("Тело:", string(body))
}

POST-запрос с JSON-телом:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type Payload struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    payload := Payload{Name: "John", Email: "john@example.com"}
    jsonData, _ := json.Marshal(payload)

    client := &http.Client{}
    req, err := http.NewRequest("POST", "https://api.example.com/users", bytes.NewBuffer(jsonData))
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    // Устанавливаем заголовки
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer your-token")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Статус:", resp.Status)
}

2. Управление таймаутами и контекстами

Одна из самых частых ошибок — отсутствие таймаутов. Без них ваш клиент может зависнуть на неограниченное время, ожидая ответа от сервера. В Go есть два основных способа управления временем ожидания.

Таймаут на уровне клиента:

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

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

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
if err != nil {
    log.Fatal(err)
}

resp, err := client.Do(req)
if err != nil {
    // Проверяем, не истек ли таймаут
    if errors.Is(err, context.DeadlineExceeded) {
        fmt.Println("Запрос превысил таймаут")
    } else {
        fmt.Println("Ошибка:", err)
    }
    return
}
defer resp.Body.Close()

Использование контекста дает больше гибкости: вы можете отменить запрос в любой момент (например, при закрытии приложения) и передавать значения через контекст.

3. Обработка ошибок и повторные попытки

Сеть ненадежна, поэтому ваш клиент должен быть готов к временным сбоям. Рассмотрим, как правильно обрабатывать ошибки и реализовать механизм retry.

Базовая обработка ошибок:

resp, err := client.Do(req)
if err != nil {
    // Проверяем тип ошибки
    if netErr, ok := err.(net.Error); ok {
        if netErr.Timeout() {
            fmt.Println("Таймаут сети")
        } else {
            fmt.Println("Сетевая ошибка:", netErr)
        }
    } else {
        fmt.Println("Другая ошибка:", err)
    }
    return
}
defer resp.Body.Close()

// Проверяем HTTP-статус
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    fmt.Printf("Ошибка HTTP: %d %s\n", resp.StatusCode, resp.Status)
    // Здесь можно прочитать тело ответа для диагностики
    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println("Тело ошибки:", string(body))
    return
}

Простая реализация retry (повторных попыток):

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

    for i := 0; i < maxRetries; i++ {
        resp, err = client.Do(req)
        if err == nil && resp.StatusCode < 500 {
            // Успех или клиентская ошибка (4xx) — не повторяем
            return resp, nil
        }

        // Если ошибка или серверная ошибка (5xx) — ждем и повторяем
        if resp != nil {
            resp.Body.Close()
        }

        // Экспоненциальная задержка
        time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond)
    }

    return resp, err
}

4. Продвинутые настройки и лучшие практики

Для серьезных проектов стандартного клиента может быть недостаточно. Рассмотрим ключевые настройки.

Настройка пула соединений:

transport := &http.Transport{
    MaxIdleConns:        100,              // Максимум idle-соединений
    MaxIdleConnsPerHost: 10,               // На один хост
    IdleConnTimeout:     90 * time.Second, // Таймаут простоя
    TLSClientConfig: &tls.Config{
        InsecureSkipVerify: false, // Для продакшена всегда false
    },
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

Логирование и мониторинг:

Блоги

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