Avatar ·

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

Всем привет! Я только начал изучать Go и пишу простой REST API сервер. Столкнулся с проблемой обработки JSON в POST запросах. У меня есть структура User: ```go type User struct { Name string Age int } ``` И хендлер: ```go func createUser(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } fmt.Fprintf(w, "User: %+v", user) } ``` Когда я отправляю POST запрос через curl: ``` curl -X POST -H "Content-Type: application/json" -d '{"name":"John","age":30}' http://localhost:8080/user ``` Я получаю пустой объект: `User: {Name: Age:0}`. Что я делаю не так? Почему поля не заполняются? Я проверил, что тело запроса приходит — если вывести r.Body в виде строки, там корректный JSON. И еще вопрос: как правильно обрабатывать ошибки при парсинге JSON? Может быть, есть какие-то best practices для продакшн кода? Заранее спасибо! <<>>

Проблема: экспортируемые поля и теги JSON

Ваша основная ошибка — отсутствие JSON тегов и понимания экспорта полей в Go. Давайте разберем по порядку.

В Go поле считается экспортируемым (доступным для внешних пакетов, включая encoding/json), только если его имя начинается с заглавной буквы. У вас Name и Age — экспортируемые, это хорошо. Но проблема в том, что json.NewDecoder ищет в JSON ключи, соответствующие именам полей в нижнем регистре по умолчанию. Ваш JSON содержит "name" и "age" (с маленькой буквы), а Go ожидает "Name" и "Age".

Решение — использовать теги структур для явного указания соответствия:

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

Теперь JSON ключи "name" и "age" будут корректно маппиться на поля структуры.

Полный рабочий пример

package main

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

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // Ограничиваем размер тела запроса для безопасности
    r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1 MB

    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        // Обработка разных типов ошибок
        switch {
        case err == io.EOF:
            http.Error(w, "Empty request body", http.StatusBadRequest)
        case err.Error() == "http: request body too large":
            http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge)
        default:
            // Проверяем, является ли ошибка синтаксической (плохой JSON)
            if syntaxErr, ok := err.(*json.SyntaxError); ok {
                http.Error(w, fmt.Sprintf("JSON syntax error at offset %d: %s", syntaxErr.Offset, syntaxErr.Error()), http.StatusBadRequest)
            } else if unmarshalErr, ok := err.(*json.UnmarshalTypeError); ok {
                http.Error(w, fmt.Sprintf("Invalid type for field '%s': expected %s", unmarshalErr.Field, unmarshalErr.Type), http.StatusBadRequest)
            } else {
                http.Error(w, "Invalid JSON", http.StatusBadRequest)
            }
        }
        return
    }

    // Проверяем обязательные поля
    if user.Name == "" {
        http.Error(w, "Field 'name' is required", http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/user", createUser)
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Best practices для продакшна

  • Ограничивайте размер тела запроса через http.MaxBytesReader — защита от DoS атак
  • Используйте детальные ошибки — различайте пустое тело, синтаксические ошибки, неверные типы данных
  • Валидируйте поля — после декодирования проверяйте обязательные поля и бизнес-логику
  • Закрывайте тело запроса — Go делает это автоматически, но в сложных сценариях можно добавить defer r.Body.Close()
  • Не доверяйте клиенту — всегда проверяйте типы данных и граничные значения
  • Используйте структуры с четкими тегами — избегайте путаницы с именами полей

Теперь ваш код будет работать корректно, а обработка ошибок станет надежной и понятной для клиентов API.

<<>>

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

Вместо json.NewDecoder можно сначала прочитать всё тело запроса, а затем распарсить его. Этот подход даёт больше контроля, особенно если нужно логировать сырой запрос или проверять его перед парсингом.

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read request body", http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    // Логируем сырой запрос для отладки
    log.Printf("Received body: %s", string(body))

    var user User
    if err := json.Unmarshal(body, &user); err != nil {
        http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
        return
    }

    // Дальнейшая обработка...
}

Плюсы:

  • Проще логировать и отлаживать
  • Можно повторно использовать тело (например, сохранить в БД сырой JSON)
  • Понятнее для новичков

Минусы:

  • Потребляет больше памяти (весь JSON в памяти)
  • Медленнее для больших JSON (нужно ждать полной загрузки)

Для большинства микросервисов с небольшими JSON (до нескольких мегабайт) этот подход вполне приемлем. Для стриминга больших данных лучше использовать json.NewDecoder.