Avatar ·

Как отправить JSON строку в POST запросе на Go?

Всем привет! Я недавно начал изучать Go и пишу небольшой REST-клиент для работы с внешним API. Мне нужно отправить POST запрос с телом в формате JSON, но я никак не могу разобраться, как это правильно сделать. У меня есть структура данных, которую я сериализую в JSON, и я хочу отправить её на сервер. Вот что я пробовал: ```go package main import ( "fmt" "net/http" "strings" ) type User struct { Name string `json:"name"` Email string `json:"email"` } func main() { user := User{Name: "Иван", Email: "ivan@example.com"} // Как мне отправить это как JSON в POST запросе? resp, err := http.Post("https://api.example.com/users", "application/json", strings.NewReader(user)) if err != nil { fmt.Println(err) } fmt.Println(resp) } ``` Я понимаю, что `strings.NewReader(user)` не работает, потому что ожидается строка, а не структура. Но как правильно преобразовать структуру в JSON и отправить её? Нужно ли использовать `json.Marshal`? А что насчёт обработки ошибок и декодирования ответа? Буду благодарен за подробный пример с объяснением. Спасибо! <<>>

Правильный способ отправки JSON в POST запросе на Go

Вы на верном пути — действительно нужно использовать json.Marshal для сериализации структуры в JSON, а затем создать из неё bytes.Reader для передачи в запрос. Вот полное решение с объяснением каждого шага.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    // 1. Создаём структуру с данными
    user := User{
        Name:  "Иван Петров",
        Email: "ivan@example.com",
    }

    // 2. Сериализуем структуру в JSON (срез байт)
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Printf("Ошибка сериализации JSON: %v\n", err)
        return
    }

    // 3. Создаём POST запрос
    resp, err := http.Post(
        "https://api.example.com/users",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        fmt.Printf("Ошибка выполнения запроса: %v\n", err)
        return
    }
    defer resp.Body.Close()

    // 4. Читаем ответ сервера
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Ошибка чтения ответа: %v\n", err)
        return
    }

    // 5. Выводим статус и тело ответа
    fmt.Printf("Статус: %d\n", resp.StatusCode)
    fmt.Printf("Ответ: %s\n", string(body))
}

Пошаговое объяснение

  • json.Marshal — преобразует структуру в срез байт формата JSON. Важно проверять ошибку, так как структура может содержать поля, которые не сериализуются (например, каналы).
  • bytes.NewBuffer — создаёт буфер из среза байт, который реализует интерфейс io.Reader, необходимый для передачи тела запроса.
  • Content-Type: application/json — обязательный заголовок, указывающий серверу формат данных.
  • defer resp.Body.Close() — обязательно закрывайте тело ответа, чтобы избежать утечки ресурсов.

Продвинутый вариант с http.Client

Для более гибкого управления запросами (таймауты, заголовки, куки) используйте http.Client:

func sendUser(user User) (*http.Response, error) {
    jsonData, err := json.Marshal(user)
    if err != nil {
        return nil, fmt.Errorf("marshal error: %w", err)
    }

    req, err := http.NewRequest(
        http.MethodPost,
        "https://api.example.com/users",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        return nil, fmt.Errorf("request creation error: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer your-token-here")

    client := &http.Client{Timeout: 10 * time.Second}
    return client.Do(req)
}

Декодирование JSON-ответа

Если сервер возвращает JSON, декодируйте его сразу в структуру:

type APIResponse struct {
    ID      int    `json:"id"`
    Message string `json:"message"`
}

var response APIResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
    fmt.Printf("Ошибка декодирования: %v\n", err)
    return
}
fmt.Printf("ID: %d, Сообщение: %s\n", response.ID, response.Message)

Использование json.NewDecoder эффективнее, чем io.ReadAll + json.Unmarshal, так как декодирует потоково, без загрузки всего ответа в память.

Типичные ошибки

  • Забыть указать Content-Type: application/json — сервер может не распознать формат.
  • Не закрыть resp.Body — утечка соединений.
  • Использовать strings.NewReader с нестроковыми данными — он принимает только строки.
  • Не обрабатывать ошибки — любая операция с сетью может провалиться.
<<>>

Альтернативный подход: использование `json.NewEncoder`

Вместо json.Marshal + bytes.NewBuffer можно использовать json.NewEncoder, который сразу записывает JSON в поток. Это особенно удобно, когда нужно отправить данные напрямую в тело запроса без промежуточного буфера:

func sendUserDirect(user User) error {
    // Создаём канал для передачи данных
    pr, pw := io.Pipe()
    go func() {
        defer pw.Close()
        if err := json.NewEncoder(pw).Encode(user); err != nil {
            fmt.Printf("Ошибка кодирования: %v\n", err)
        }
    }()

    resp, err := http.Post("https://api.example.com/users", "application/json", pr)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // Обработка ответа...
    return nil
}

Этот метод полезен для потоковой передачи больших JSON-объектов, так как не требует хранения всего JSON в памяти. Однако для большинства случаев первый вариант (с json.Marshal) проще и надёжнее.

Ещё один совет: если структура содержит поля, которые могут быть nil, используйте указатели или опциональные типы. Например:

type User struct {
    Name  string  `json:"name"`
    Email string  `json:"email,omitempty"`
    Phone *string `json:"phone,omitempty"`
}

Тег omitempty исключает пустые поля из JSON, а указатель позволяет различать отсутствие поля и пустое значение.