Отправляем JSON в POST-запросе на Go: от "как это вообще работает" до "сделал и забыл"
Вы когда-нибудь залипали в Stack Overflow, пытаясь понять, почему ваш POST-запрос с JSON на Go не принимает сервер? Знакомо: вроде бы всё правильно, а в ответ — 400 Bad Request или пустое тело. Оказывается, в Go есть несколько способов отправить JSON, и выбор правильного — это не просто вопрос вкуса, а вопрос стабильности вашего микросервиса.
В этой статье мы не просто покажем код. Мы разберём три подхода: от наивного (который работает, но опасен) до продакшен-решения с правильной обработкой ошибок и контекстами. Если вы пишете на Go для API, ботов или интеграций — это обязательное чтение.
Почему эта тема такая популярная?
Go — язык для сетевых сервисов. Рано или поздно любой разработчик сталкивается с задачей: "отправить JSON в теле POST-запроса". Казалось бы, что сложного? Но нюансы есть:
- Нужно правильно сериализовать структуру в JSON (маршалинг).
- Важно не забыть заголовок
Content-Type: application/json. - Необходимо корректно закрыть тело ответа.
- Хочется избежать утечек памяти и паник.
Давайте разберём каждый подход на живых примерах.
Подход 1: "На коленке" — bytes.NewBuffer + json.Marshal
Это самый простой способ. Берём структуру, маршалим её в байты, кладём в буфер и отправляем. Многие начинающие Go-разработчики пишут именно так.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
user := User{Name: "Иван Петров", Email: "ivan@example.com"}
// Шаг 1: маршалим структуру в JSON
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Ошибка маршалинга:", err)
return
}
// Шаг 2: создаём POST-запрос
resp, err := http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
fmt.Println("Ошибка запроса:", err)
return
}
defer resp.Body.Close()
fmt.Println("Статус:", resp.Status)
}
Плюсы и минусы этого подхода
- Плюс: Минимум кода, понятно новичку.
- Минус: Нет гибкости. Вы не можете добавить кастомные заголовки (авторизация, trace-id). Нет контроля над таймаутами. Если сервер вернёт ошибку — вы её не обработаете.
- Минус: Потенциальная проблема: если
json.Marshalвернёт ошибку, вы её поймаете, но в реальном коде часто забывают проверять.
Этот способ годится для прототипов и тестов. Для продакшена — нет.
Подход 2: "Правильный" — http.NewRequest + json.NewEncoder
Go-сообщество рекомендует использовать http.NewRequest и json.NewEncoder. Почему? Потому что это даёт полный контроль над запросом и эффективнее по памяти.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type Order struct {
ID int `json:"id"`
Amount float64 `json:"amount"`
Items []string `json:"items"`
}
func sendOrder(order Order) error {
// Создаём буфер и энкодер
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
// Кодируем напрямую в буфер (без промежуточного []byte)
if err := enc.Encode(order); err != nil {
return fmt.Errorf("ошибка кодирования JSON: %w", err)
}
// Создаём запрос
req, err := http.NewRequest("POST", "https://api.example.com/orders", &buf)
if err != nil {
return fmt.Errorf("ошибка создания запроса: %w", err)
}
// Устанавливаем заголовки
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer your-token-here")
// Клиент с таймаутом
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("ошибка выполнения запроса: %w", err)
}
defer resp.Body.Close()
// Проверяем статус ответа
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("сервер вернул ошибку: %s", resp.Status)
}
return nil
}
func main() {
order := Order{
ID: 42,
Amount: 299.99,
Items: []string{"Книга по Go", "Ноутбук"},
}
if err := sendOrder(order); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Заказ успешно отправлен!")
}
}
Почему это лучше?
- json.NewEncoder пишет JSON напрямую в
io.Writer(наш буфер). Это эффективнее, чемjson.Marshal, который создаёт промежуточный срез байтов. - Вы сами контролируете заголовки, таймауты и обработку ошибок.
- Код легко расширяется: можно добавить логирование, ретраи, метрики.
Подход 3: "Продакшен" — обёртка с контекстом и ретраями
В реальных микросервисах вам понадобится передавать контекст (для отмены запроса), делать ретраи при временных ошибках и логировать. Вот как выглядит зрелый подход.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
)
type APIClient struct {
baseURL string
httpClient *http.Client
token string
}
func NewAPIClient(baseURL, token string) *APIClient {
return &APIClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
},
},
token: token,
}
}
// PostJSON отправляет JSON в POST-запросе с поддержкой контекста
func (c *APIClient) PostJSON(ctx context.Context, endpoint string, payload interface{}) ([]byte, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
return nil, fmt.Errorf("encode payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+endpoint, &buf)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.token)
// Логируем запрос (в продакшене используйте структурированное логирование)
log.Printf("POST %s с телом %d байт", endpoint, buf.Len())
resp,