JSON API
JSON API
Большинство современных Go-сервисов — это JSON API. Стандартный пакет encoding/json покрывает 95% случаев. Разберём его устройство и типичные паттерны.
Теги структур и маппинг полей
Go автоматически маппит поля структуры на JSON по имени. Но имена в Go — PascalCase, в JSON принято — camelCase или snake_case. Теги решают эту проблему:
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
// 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":""}// 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 без промежуточного буфера:
// Читаем 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 — пиши его в каждом проекте. Без него копируешь три строки в каждом обработчике.
Структуры запроса и ответа
Хорошая практика — отдельные типы для входящих и исходящих данных:
// Запрос — только то, что приходит от клиента
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 только парсит структуру, но не валидирует значения. Валидацию пиши сам:
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". Но для большинства случаев ручная валидация читается лучше.
Полный обработчик: от запроса до ответа
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:
// Успех 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"} // никогда не раскрывай детали!Заведи обёртки:
type SuccessResponse struct {
Data any `json:"data"`
}
type ErrorResponse struct {
Error string `json:"error"`
Fields any `json:"fields,omitempty"`
}DisallowUnknownFields
По умолчанию json.Decoder игнорирует незнакомые поля. Иногда это нежелательно:
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
// вернёт ошибку если в JSON есть поле которого нет в структуре
}Используй это в строгих API где лишние поля — признак ошибки клиента, но не в публичных API — это сломает обратную совместимость.
Числа в JSON: подводный камень
// Проблема: 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 — все используют этот паттерн.
json.Marshal превращает Go-структуру в JSON. Теги управляют именами полей.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}{
"id": 1,
"name": "Alice",
"email": "a@ex.com"
}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))