Введение: почему 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