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

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

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

Начать курс

Введение: почему HTTP-клиент в Go заслуживает внимания

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

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

Основы работы с HTTP-клиентом в Go

Библиотека net/http в Go спроектирована с учётом производительности. В отличие от многих других языков, где каждый запрос создаёт новое подключение, Go использует пул соединений (connection pool) по умолчанию. Это означает, что TCP-соединения переиспользуются, что значительно ускоряет работу.

Простейший GET-запрос

Начнём с самого простого примера — отправка GET-запроса с помощью функции http.Get():

package main

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

func main() {
    // Простой GET-запрос
    resp, err := http.Get("https://api.github.com")
    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.Close()). Используйте defer для гарантированного закрытия, даже если возникнет ошибка.

GET-запрос с параметрами и заголовками

Для более сложных запросов лучше использовать полный синтаксис с созданием объекта http.Request:

package main

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

func main() {
    // Создаём новый клиент
    client := &http.Client{
        Timeout: 10 * time.Second,
    }

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

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

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

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

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("Ответ: %s\n", string(body))
}

Работа с POST-запросами и JSON

POST-запросы с JSON-телом — стандартная задача при работе с REST API. Go предоставляет удобные инструменты для сериализации данных.

Отправка JSON с помощью POST

package main

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

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

func main() {
    user := User{
        Name:  "Иван Петров",
        Email: "ivan@example.com",
        Age:   30,
    }

    // Сериализуем структуру в JSON
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Printf("Ошибка сериализации JSON: %v\n", err)
        return
    }

    client := &http.Client{Timeout: 5 * time.Second}

    // Создаём POST-запрос с JSON-телом
    resp, err := client.Post(
        "https://api.example.com/users",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        fmt.Printf("Ошибка POST-запроса: %v\n", err)
        return
    }
    defer resp.Body.Close()

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

Десериализация JSON-ответа

Обработка JSON-ответа от сервера — не менее важная задача:

func parseUserResponse(resp *http.Response) (*User, error) {
    defer resp.Body.Close()

    var user User
    // Используем Decoder для потоковой обработки JSON
    err := json.NewDecoder(resp.Body).Decode(&user)
    if err != nil {
        return nil, fmt.Errorf("ошибка декодирования JSON: %w", err)
    }

    return &user, nil
}

Управление таймаутами и пулом соединений

Правильная настройка HTTP-клиента критически важна для production-приложений. Без таймаутов ваше приложение может зависнуть навсегда, ожидая ответа от недоступного сервера.

Создание продвинутого клиента

package main

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

func CreateAdvancedClient() *http.Client {
    transport := &http.Transport{
        // Максимальное количество соединений к одному хосту
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 20,
        MaxConnsPerHost:     50,

        // Таймауты
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,

        // Настройка TCP-соединения
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
    }

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

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

Надёжное приложение должно уметь обрабатывать временные сбои:

func MakeRequestWithRetry(url string, maxRetries int) (*http.Response, error) {
    client := CreateAdvancedClient()

    var lastErr error
    for i := 0; i < maxRetries; i++ {
        resp, err := client.Get(url)
        if err == nil {
            return resp, nil                

Блоги

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