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

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

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

Начать курс

Введение

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

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

1. Основы: GET-запрос и чтение ответа

Самый простой способ выполнить GET-запрос — использовать функцию http.Get(). Однако для продакшн-кода рекомендуется создавать собственный экземпляр http.Client — это даёт больше контроля над поведением клиента.

Простой GET-запрос

package main

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

func main() {
    // Создаём клиента (пока с настройками по умолчанию)
    client := &http.Client{}
    
    // Выполняем GET-запрос
    resp, err := client.Get("https://api.example.com/data")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ошибка запроса: %v\n", err)
        os.Exit(1)
    }
    // ВАЖНО: закрываем тело ответа
    defer resp.Body.Close()

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

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

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

  • defer resp.Body.Close() — обязательно закрывайте тело ответа, иначе произойдёт утечка ресурсов (соединение не вернётся в пул).
  • Всегда проверяйте err — сеть нестабильна, и запрос может завершиться ошибкой.
  • Проверяйте HTTP-статус ответа (resp.StatusCode), чтобы отличить успех (2xx) от ошибок сервера (4xx, 5xx).

2. POST-запрос с телом и заголовками

Для отправки данных на сервер (например, JSON) используется http.Post() или более гибкий метод http.NewRequest() с последующим вызовом client.Do(). Второй способ предпочтительнее, так как позволяет задать заголовки и другие параметры.

POST с JSON данными

package main

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

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

func main() {
    client := &http.Client{}

    // Подготавливаем данные
    user := User{Name: "Иван Петров", Email: "ivan@example.com"}
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ошибка сериализации JSON: %v\n", err)
        os.Exit(1)
    }

    // Создаём запрос
    req, err := http.NewRequest("POST", "https://api.example.com/users", bytes.NewBuffer(jsonData))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ошибка создания запроса: %v\n", err)
        os.Exit(1)
    }

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

    // Отправляем запрос
    resp, err := client.Do(req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ошибка запроса: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    // Проверяем статус
    if resp.StatusCode != http.StatusCreated {
        fmt.Fprintf(os.Stderr, "Сервер вернул ошибку: %s\n", resp.Status)
        os.Exit(1)
    }

    fmt.Println("Пользователь успешно создан!")
}

Важно: Для отправки форм (application/x-www-form-urlencoded) используйте url.Values и strings.NewReader(encodedData). Для multipart-запросов (загрузка файлов) — bytes.Buffer и mime/multipart.

3. Таймауты и управление соединениями

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

Настройка таймаутов

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    client := &http.Client{
        // Таймаут на весь запрос (включая соединение, редиректы, чтение тела)
        Timeout: 10 * time.Second,
        
        // Таймаут на установку TCP-соединения (через Transport)
        Transport: &http.Transport{
            DialContext: (&net.Dialer{
                Timeout: 5 * time.Second,
            }).DialContext,
            // Максимальное время ожидания ответа сервера (TTFB)
            ResponseHeaderTimeout: 5 * time.Second,
            // Максимальное количество соединений к одному хосту
            MaxIdleConnsPerHost: 10,
            // Время жизни бездействующего соединения
            IdleConnTimeout: 30 * time.Second,
        },
    }

    resp, err := client.Get("https://api.example.com/data")
    if err != nil {
        // Таймаут вызовет ошибку: "Client.Timeout exceeded"
        fmt.Printf("Ошибка (возможно таймаут): %v\n", err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("Статус: %s\n", resp.Status)
}

Рекомендации:

  • Всегда устанавливайте Timeout для клиента — это спасёт от зависаний.
  • Если ваш клиент делает много запросов к одному серверу, настройте MaxIdleConnsPerHost для переиспользования соединений.
  • Используйте context.WithTimeout для тонкого контроля таймаутов на уровне отдельных запросов.

Блоги

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