POST-запрос с JSON в Go: от новичка до продакшена

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Отправляем 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,                

Блоги

Рекомендаций по книгам