Введение: Почему важно уметь работать с 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)