Введение
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для тонкого контроля таймаутов на уровне отдельных запросов.