Avatar ·

Как правильно отдавать JSON в Go: полный гайд с примерами

Всем привет! Я только начал изучать Go и столкнулся с проблемой: нужно написать простой HTTP-сервер, который на GET-запрос по адресу /api/user возвращает JSON с данными пользователя. Я читал, что в Go есть встроенный пакет encoding/json, но никак не могу понять, как правильно сериализовать структуру в JSON и отправить её в ответе. Пробовал сделать так: ```go package main import ( "fmt" "net/http" ) type User struct { Name string Age int } func userHandler(w http.ResponseWriter, r *http.Request) { user := User{Name: "Анна", Age: 28} fmt.Fprintf(w, "%v", user) } func main() { http.HandleFunc("/api/user", userHandler) http.ListenAndServe(":8080", nil) } ``` Но в ответе приходит что-то вроде `{Анна 28}`, а не `{"Name":"Анна","Age":28}`. Как правильно маршалить структуру в JSON и установить правильный заголовок Content-Type? Нужно ли обрабатывать ошибки? И что делать, если хочется, чтобы поля назывались в camelCase, например `userName`, а не `Name`? Заранее спасибо за помощь! <<>>

Правильный способ отдачи JSON в Go

Вы на верном пути! Проблема в том, что fmt.Fprintf форматирует структуру в её текстовое представление, а не в JSON. Для корректной работы с JSON нужно использовать пакет encoding/json. Вот полное решение вашей задачи.

Базовое решение

package main

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

type User struct {
    Name string `json:"userName"` // тег для переименования поля
    Age  int    `json:"userAge"`
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    user := User{
        Name: "Анна",
        Age:  28,
    }

    w.Header().Set("Content-Type", "application/json")
    
    if err := json.NewEncoder(w).Encode(user); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

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

Ключевые моменты

  • Теги структур: json:"userName" позволяет задать имя поля в JSON, отличное от имени поля в Go. Используйте camelCase или snake_case — как вам удобнее.
  • Установка заголовка: w.Header().Set("Content-Type", "application/json") сообщает клиенту, что ответ — это JSON.
  • Кодирование через NewEncoder: json.NewEncoder(w).Encode(user) сразу пишет JSON в ResponseWriter. Это эффективно для потоковой передачи.
  • Обработка ошибок: всегда проверяйте ошибку. Если структура содержит несериализуемые поля (например, каналы), Encode вернёт ошибку.

Альтернативный способ: Marshal + Write

func userHandler(w http.ResponseWriter, r *http.Request) {
    user := User{Name: "Анна", Age: 28}
    
    jsonData, err := json.Marshal(user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.Write(jsonData)
}

Этот способ сначала создаёт []byte, потом пишет его в ответ. Для маленьких данных разницы нет, но для больших — лучше использовать NewEncoder.

Дополнительные советы

  • Пропуск пустых полей: используйте json:"userName,omitempty" — если поле пустое, оно не попадёт в JSON.
  • Игнорирование поля: json:"-" полностью исключает поле из сериализации.
  • Кастомная сериализация: реализуйте интерфейс json.Marshaler для сложных типов.
  • Срезы и карты: они тоже отлично сериализуются. Например, []string{"red", "green"} станет ["red","green"].

Проверка результата

После запуска сервера откройте в браузере http://localhost:8080/api/user или выполните:

curl http://localhost:8080/api/user

Вы увидите: {"userName":"Анна","userAge":28}. Всё работает!

Помните: Go — строго типизированный язык, поэтому структура для JSON должна быть чётко определена. Для динамических данных используйте map[string]interface{}, но старайтесь избегать этого — теряется типобезопасность.

<<>>

Использование map[string]interface{} для динамических JSON

Если структура данных заранее неизвестна или меняется в рантайме, можно использовать map[string]interface{}:

func userHandler(w http.ResponseWriter, r *http.Request) {
    user := map[string]interface{}{
        "userName": "Анна",
        "userAge":  28,
        "isAdmin":  true,
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

Такой подход удобен для быстрого прототипирования, но имеет недостатки:

  • Нет проверки типов на этапе компиляции — ошибки всплывут только в рантайме.
  • Ключи могут быть неконсистентными (опечатки в названиях).
  • Сложнее поддерживать код в больших проектах.

Когда использовать

map[string]interface{} подходит для:

  • Прокси-серверов, которые передают JSON без изменений.
  • Гибридных ответов, где часть полей варьируется.
  • Быстрых экспериментов.

Но для production-кода всегда предпочитайте строгие структуры с тегами — это даёт читаемость, безопасность и производительность.