Как правильно обработать JSON POST запрос в Go?
Проблема: экспортируемые поля и теги 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.