Модуль
1HTTP-сервер на Go2JSON API← вы здесь3Graceful Shutdown4Финальный проект
Урок 2~12 минут

JSON API

JSON API

Большинство современных Go-сервисов — это JSON API. Стандартный пакет encoding/json покрывает 95% случаев. Разберём его устройство и типичные паттерны.

Теги структур и маппинг полей

Go автоматически маппит поля структуры на JSON по имени. Но имена в Go — PascalCase, в JSON принято — camelCase или snake_case. Теги решают эту проблему:

go
type User struct {
    ID        int    `json:"id"`
    FirstName string `json:"first_name"`
    Email     string `json:"email,omitempty"`
    Password  string `json:"-"`            // никогда не попадёт в JSON
    CreatedAt string `json:"created_at"`
}

Опции в теге (через запятую после имени):

  • omitempty — пропускает поле если значение "пустое" (0, "", false, nil, пустой slice/map)
  • - — всегда пропускать (пароли, внутренние поля)
  • string — сериализовать число как строку (json:"id,string""42" вместо 42)

Marshal и Unmarshal

go
// Struct -> JSON bytes
user := User{ID: 1, FirstName: "Alice", Email: "alice@example.com"}
data, err := json.Marshal(user)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
// {"id":1,"first_name":"Alice","email":"alice@example.com","created_at":""}
go
// JSON bytes -> Struct
var decoded User
err := json.Unmarshal(data, &decoded)
if err != nil {
    // Синтаксическая ошибка JSON или несовместимый тип
    var syntaxErr *json.SyntaxError
    if errors.As(err, &syntaxErr) {
        log.Printf("syntax error at offset %d", syntaxErr.Offset)
    }
}

json.Unmarshal игнорирует неизвестные поля — это удобно при добавлении новых полей в API без поломки старых клиентов.

Encoder/Decoder для HTTP

В HTTP-сервере используй json.NewEncoder/json.NewDecoder вместо Marshal/Unmarshal — они работают напрямую с io.Reader/io.Writer без промежуточного буфера:

go
// Читаем JSON из тела запроса
func createUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "invalid JSON", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()
 
    // обработка...
}
 
// Пишем JSON в ответ
func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

Хелпер writeJSON — пиши его в каждом проекте. Без него копируешь три строки в каждом обработчике.

Структуры запроса и ответа

Хорошая практика — отдельные типы для входящих и исходящих данных:

go
// Запрос — только то, что приходит от клиента
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}
 
// Ответ — только то, что безопасно отдавать клиенту
type UserResponse struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    CreatedAt string `json:"created_at"`
}
 
// Никогда не возвращай модель БД напрямую!
// type User struct { Password string ... }
// json.Encode(user) — утечка пароля

Это кажется избыточным, но защищает от случайной утечки чувствительных данных.

Валидация входящих данных

encoding/json только парсит структуру, но не валидирует значения. Валидацию пиши сам:

go
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}
 
type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}
 
func (r *CreateUserRequest) Validate() []ValidationError {
    var errs []ValidationError
 
    if r.Name == "" {
        errs = append(errs, ValidationError{"name", "required"})
    } else if len(r.Name) > 100 {
        errs = append(errs, ValidationError{"name", "max 100 chars"})
    }
 
    if r.Email == "" {
        errs = append(errs, ValidationError{"email", "required"})
    } else if !strings.Contains(r.Email, "@") {
        errs = append(errs, ValidationError{"email", "invalid format"})
    }
 
    return errs
}

Для серьёзной валидации есть пакет go-playground/validator — декларативные теги типа validate:"required,email,max=100". Но для большинства случаев ручная валидация читается лучше.

Полный обработчик: от запроса до ответа

go
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
    // 1. Декодируем тело
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeJSON(w, http.StatusBadRequest, map[string]string{
            "error": "invalid JSON: " + err.Error(),
        })
        return
    }
 
    // 2. Валидация
    if errs := req.Validate(); len(errs) > 0 {
        writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
            "error":  "validation failed",
            "fields": errs,
        })
        return
    }
 
    // 3. Бизнес-логика
    user, err := h.db.CreateUser(r.Context(), req.Name, req.Email)
    if err != nil {
        if errors.Is(err, ErrEmailTaken) {
            writeJSON(w, http.StatusConflict, map[string]string{
                "error": "email already taken",
            })
            return
        }
        log.Printf("create user: %v", err)
        writeJSON(w, http.StatusInternalServerError, map[string]string{
            "error": "internal server error",
        })
        return
    }
 
    // 4. Ответ
    writeJSON(w, http.StatusCreated, UserResponse{
        ID:        user.ID,
        Name:      user.Name,
        Email:     user.Email,
        CreatedAt: user.CreatedAt.Format(time.RFC3339),
    })
}

Обрати внимание на обработку ошибок: каждый return после ошибки — обязательный. Без него функция продолжит выполнение и клиент получит два ответа (Go запаникует или отправит мусор).

Паттерны ответов API

Консистентная структура ответов — залог хорошего API:

go
// Успех 200
{"data": {"id": 1, "name": "Alice"}}
 
// Создан 201
{"data": {"id": 42, "name": "Bob"}}
 
// Ошибка клиента 400/422
{"error": "validation failed", "fields": [{"field": "email", "message": "required"}]}
 
// Не найдено 404
{"error": "user not found"}
 
// Конфликт 409
{"error": "email already taken"}
 
// Серверная ошибка 500
{"error": "internal server error"}  // никогда не раскрывай детали!

Заведи обёртки:

go
type SuccessResponse struct {
    Data any `json:"data"`
}
 
type ErrorResponse struct {
    Error  string `json:"error"`
    Fields any    `json:"fields,omitempty"`
}

DisallowUnknownFields

По умолчанию json.Decoder игнорирует незнакомые поля. Иногда это нежелательно:

go
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
 
if err := decoder.Decode(&req); err != nil {
    // вернёт ошибку если в JSON есть поле которого нет в структуре
}

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

Числа в JSON: подводный камень

go
// Проблема: int64 в JavaScript — максимум 2^53
type Response struct {
    ID int64 `json:"id"` // 9007199254740993 — потеря точности в JS!
}
 
// Решение: передавать как строку
type Response struct {
    ID int64 `json:"id,string"` // "9007199254740993"
}

Если твой API используют JS-клиенты — ID и другие большие int64 передавай как строки. Twitter, Snowflake IDs — все используют этот паттерн.

encoding/json в Go работает через reflection. Это медленнее кодогенерации, но для большинства сервисов достаточно. Если нужна скорость — смотри на easyjson или jsoniter.

json.Marshal превращает Go-структуру в JSON. Теги управляют именами полей.

Структура User
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
}
Поля структуры
ID int json:"id"
Name string json:"name"
Email string json:"email,omitempty"omitempty
JSON json.Marshal
{
  "id": 1,
  "name": "Alice",
  "email": "a@ex.com"
}
Паттерн обработки ошибок Marshal
u := User{ID: 1, Name: "Alice", Email: "a@ex.com"}
data, err := json.Marshal(u)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
🎯
Миссия 1 из 4
Как называется тег структуры Go для задания имени поля в JSON?