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,
}
Логирование и мониторинг: