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

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

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

Начать курс

Введение: Почему важно уметь работать с HTTP в Go

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

В этой статье мы разберем создание HTTP клиента на Go «от А до Я». Вы узнаете, как правильно отправлять GET и POST запросы, управлять заголовками, настраивать таймауты и обрабатывать ошибки. Все примеры кода готовы к использованию.

1. Основы: создание клиента и первый GET-запрос

В Go HTTP-клиент — это структура http.Client. Вы можете использовать глобальный клиент http.DefaultClient или создать свой с кастомными настройками. Начнем с простого GET-запроса.

package main

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

func main() {
    // Используем встроенный клиент
    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        fmt.Printf("Ошибка запроса: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close() // ВАЖНО: всегда закрывать тело ответа!

    // Читаем тело ответа
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Ошибка чтения ответа: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Статус: %s\n", resp.Status)
    fmt.Printf("Тело: %s\n", string(body))
}

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

  • Всегда закрывайте resp.Body через defer. Иначе произойдет утечка ресурсов (соединения не вернутся в пул).
  • Проверяйте статус ответа (resp.StatusCode). Код 200 — не единственный успешный (бывают 201, 204 и т.д.).
  • io.ReadAll читает все тело в память. Для больших ответов используйте io.Copy или потоковую обработку.

2. Продвинутый клиент: таймауты, заголовки и кастомные настройки

Глобальный клиент не имеет таймаутов по умолчанию — это может привести к зависанию программы. Всегда создавайте свой клиент с настройками.

2.1. Создание клиента с таймаутом

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

resp, err := client.Get("https://api.example.com/data")

Также можно настроить таймауты на разных уровнях через Transport:

transport := &http.Transport{
    DialContext: (&net.Dialer{
        Timeout:   5 * time.Second, // Таймаут соединения
        KeepAlive: 30 * time.Second,
    }).DialContext,
    TLSHandshakeTimeout:   5 * time.Second,
    ResponseHeaderTimeout: 5 * time.Second,
}

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

2.2. Добавление заголовков и параметров

Для сложных запросов используйте http.NewRequest:

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

// Добавляем заголовки
req.Header.Set("Authorization", "Bearer your-token")
req.Header.Set("Accept", "application/json")

// Добавляем query-параметры
q := req.URL.Query()
q.Add("query", "golang")
q.Add("page", "1")
req.URL.RawQuery = q.Encode()

resp, err := client.Do(req)

3. Работа с POST и другими методами (JSON, формы)

POST-запросы требуют передачи тела. Рассмотрим два популярных формата: JSON и URL-encoded форма.

3.1. POST с JSON

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

data := RequestData{
    Name:  "Иван",
    Email: "ivan@example.com",
}

jsonData, err := json.Marshal(data)
if err != nil {
    log.Fatal(err)
}

resp, err := client.Post(
    "https://api.example.com/users",
    "application/json",
    bytes.NewBuffer(jsonData),
)

3.2. POST с URL-encoded формой

formData := url.Values{
    "username": {"ivan"},
    "password": {"secret123"},
}

resp, err := client.PostForm("https://api.example.com/login", formData)

3.3. Использование http.NewRequest для POST

Этот способ дает больше контроля:

req, err := http.NewRequest("POST", "https://api.example.com/upload", bytes.NewBuffer(fileBytes))
req.Header.Set("Content-Type", "application/octet-stream")

resp, err := client.Do(req)

4. Обработка ошибок, редиректов и повторные попытки

Надежный HTTP-клиент должен корректно обрабатывать различные сценарии.

4.1. Проверка статуса ответа

resp, err := client.Get(url)
if err != nil {
    // Ошибка сети, таймаут, DNS и т.д.
    return fmt.Errorf("ошибка запроса: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    body, _ := io.ReadAll(resp.Body)
    return fmt.Errorf("сервер вернул ошибку %d: %s", resp.StatusCode, string(body))
}

4.2. Управление редиректами

По умолчанию клиент следует редиректам (до 10 раз). Это можно изменить:

client := &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        if len(via) >= 3 {
            return fmt.Errorf("слишком много редиректов")
        }
        // Можно модифицировать запрос перед редиректом
        req.Header.Set("X-Redirect", "true")
        return nil
    },
}

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

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

Блоги

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