Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 9 из 25 ~20 мин +50 XP

Работа с JSON

Тема «fiber json» — про байты, которые ходят между клиентом и вашим API. Восемь уроков мы отвечали литералами fiber.Map и не трогали входящие тела; сегодня вскрываем механику обеих сторон: как c.JSON сериализует ответ и ставит Content-Type, что делают теги json и omitempty, из чего состоят коды ответов Status и SendStatus — и как Fiber v3 разбирает входящий JSON методом Bind, который в комментариях исходников назван прямой заменой BodyParser из второго поколения.

Содержание урока · 14 разделов

fiber json: байты в обе стороны#

Каждый хендлер этого курса заканчивался вызовом c.JSON — пора вскрыть json-слой Fiber построчно. JSON здесь работает в два направления. Исходящее: хендлер отдаёт значение Go, фреймворк превращает его в байты и подписывает заголовком Content-Type: application/json; charset=utf-8. Входящее: клиент присылает тело, и его надо разобрать обратно в структуру Go — с проверкой, что Content-Type и содержимое совпали.

Обещания мы уже раздавали: в первом уроке среди «что есть в коробке» был пункт «JSON: c.JSON для ответов, BodyParser для разбора тела запроса», а таблица четвёртого урока откладывала разбор тел со словами «BodyParser — в уроке про JSON». Платим по счетам, но с поправкой на версию: в v3 BodyParser сменился методом Bind — слово из tutorials второго поколения мы встретим и аккуратно переведём. Сверять всё будем с исходниками тега v3.5.0: res.go для ответов, ctx.go и bind.go для разбора, constants.go для кодов.

c.JSON под микроскопом: три шага на wire

GO
package main

import (
        "log"

        "github.com/gofiber/fiber/v3"
)

func main() {
        app := fiber.New()

        app.Get("/healthz", func(c fiber.Ctx) error {
                // сериализация, тело и Content-Type — одним вызовом
                return c.JSON(fiber.Map{"ok": true})
        })

        log.Fatal(app.Listen(":8080"))
}

Сигнатура из res.go: func (r *DefaultRes) JSON(data any, ctype ...string) error. Внутри три шага. Первый — app.config.JSONEncoder(data): кодировщик берётся из конфига приложения, а app.go назначает json.Marshal, если поле не задано. Второй — response.SetBodyRaw(raw): готовые байты ложатся в тело без повторного копирования. Третий — Content-Type: если второй аргумент не передан, ставится константа MIMEApplicationJSONCharsetUTF8, за которой стоит строка application/json; charset=utf-8; передадите ctype — уедет ваша строка. curl -i покажет оба следа сразу: заголовок и тело {"ok":true}.

fiber.Map против структур: форма ответа#

fiber.Map объявлен в app.go одной строкой: type Map map[string]any. Это карта «что угодно» — и в этом сила, и слабость. Сила: {"ok": true} для health-check или {"error": "неверный API-ключ"} для стража из седьмого урока пишутся мгновенно, без отдельного типа. Слабость проявляется, когда ответ — это данные: имена ключей живут в строках, опечатку «tiel» компилятор не заметит, форма ответа нигде не задокументирована, а каждое значение — any, то есть без защиты типов.

Есть и приятный бонус у карт, о котором стоит знать: encoding/json сортирует ключи карты по алфавиту, поэтому fiber.Map даёт байтово предсказуемый вывод — полезно для хешей и диффов. Но порядок этот — алфавитный, а не смысловой. Структура работает наоборот: порядок полей в JSON повторяет порядок объявления, поля и типы фиксирует компилятор, а форму видно прямо в коде. Практическое правило: Map — для мелких ответов без формы данных; всё, что называется существительным (задача, профиль, заказ), — структура с тегами json.

Task: первая структура курса Fiber

GO
// Task — форма данных API: ключи и порядок задаёт тег json
type Task struct {
        ID        int    `json:"id"`
        Title     string `json:"title"`
        Done      bool   `json:"done"`
        Priority  string `json:"priority,omitempty"`
        Estimate  int    `json:"estimate,omitempty"`
        CreatedAt string `json:"created_at,omitempty"`
}

// GET /api/tasks — список задач
api.Get("/tasks", func(c fiber.Ctx) error {
        tasks := []Task{
                {ID: 1, Title: "починить шлюз"},
                {ID: 2, Title: "обновить сертификат", Done: true, Priority: "high"},
        }
        // данные завёрнуты в items, как во всех уроках курса
        return c.JSON(fiber.Map{"items": tasks})
})

Теги json — это договор не с Fiber, а с encoding/json, который тот вызывает по умолчанию. Имя ключа берётся из тега, а не из поля: title, а не Title. Стиль ключей выбираете вы: created_at — snake_case, написанный руками, автоматического переименования нет. Estimate — целое число часов, добавлено нарочно: на нём мы поймаем ошибку типов, когда клиент пришлёт строку. Приоритет и дата помечены omitempty — что делает этот флаг и сколько он стоит, — следующий блок. Вывод эндпоинта: {"items":[{"id":1,"title":"починить шлюз","done":false},...]} — done печатается честно, ведь без omitempty false остаётся false.

Теги json: omitempty и цена тишины#

omitempty — самое экономное слово в словаре тегов: если значение пустое по правилам encoding/json, поле не сериализуется вовсе. Пустое — это ноль для чисел, false для булевых, пустая строка, nil для указателей, интерфейсов, срезов и карт, а также любой срез или карта с len == 0. Черновик без приоритета не тащит ключ priority в каждом элементе списка, ответ дышит свободнее, а снапшоты ответов в тестах перестают трястись от пустых полей.

У экономии есть цена, и о ней принято забывать. Поле, которого нет, неотличимо для клиента от поля, о котором он не знает: старая версия мобильного приложения, проверяющая наличие ключа, поведёт себя иначе, чем с "priority": "". Форма ответа становится зависимой от данных — ровно до момента, когда кто-то начнёт строить на ней интерфейс. Поэтому omitempty уместен на необязательных полях и вреден на смысловых: done с omitempty превращал бы «задача не сделана» в «задача без статуса».

Два соседних приёма из того же словаря. Запятая с флагом без имени — json:",omitempty" — допустима: имя берётся из поля, но ключ станет Priority в PascalCase, так что имя в теге лучше писать всегда. А тег json:"-" прячет поле целиком — и при сериализации, и при разборе: так наружу не уезжают пароли и внутренние метки. Всё это — семантика encoding/json, общая для всего Go-мира: у gin из параллельного курса теги работают точно так же, отличие фреймворков — в методах, а не в тегах.

Проверьте себя: omitempty в деле

Хендлер возвращает Task{ID: 3, Title: "позвонить", Done: false} — Priority, Estimate и CreatedAt помечены omitempty, Done без флагов. Какой JSON уедет?

Fiber против gin: честное сравнение JSON-методов#

В пятом уроке курса gin разобрано целое семейство ответов: IndentedJSON с отступами для отладки, PureJSON без HTML-экранирования, SecureJSON с префиксом против дед hijacking и AsciiJSON с \u-эскейпами. Сверим с тегом v3.5.0: в Fiber v3 ни одного из этих методов нет — c.JSON ровно один, никаких IndentedJSON или PureJSON на Ctx не существует (подробное сравнение — в пятом уроке курса gin).

Честная картина по обе стороны. Экранирование одинаковое: json.Marshal превращает <, > и & в \u003c, \u003e и \u0026 — и gin, и Fiber в базовом методе этого не отключают; переключателя уровня PureJSON (SetEscapeHTML(false)) в комплекте Fiber нет. Красивая печать решается иначе — подменой кодировщика: fiber.Config{JSONEncoder: json.MarshalIndent-обёртка} — поле типа utils.JSONMarshal с дефолтом json.Marshal, и все c.JSON начнут отвечать с отступами. Разумно держать такой конфиг у /debug-эндпоинтов или включать по переменной окружения: на каждый ответ процессора тратится, а тело растёт. Взамен v3 даёт то, чего нет у gin: соседей c.JSON тем же паттерном — JSONP с поддержкой callback, MsgPack и CBOR — бинарные форматы с собственными методами и кодировщиками из конфига. Content-Type application/json; charset=utf-8 — общий знаменатель обоих фреймворков; там, где gin экономил байты PureJSON, Fiber предлагает посчитать, нужны ли вам точные байты вообще.

Что уедет в теле ответа?

Прочитайте код и выберите, что он выведет:

type Task struct {
        ID       int    `json:"id"`
        Title    string `json:"title"`
        Priority string `json:"priority,omitempty"`
        Note     string `json:"note,omitempty"`
}

// GET /api/tasks/3 возвращает напрямую структуру:
return c.JSON(Task{ID: 3, Title: "позвонить <клиенту>", Note: ""})

Коды ответов: Status, SendStatus и статусные фразы#

JSON-ответ — это заголовки, код и тело. Разберём последние два инструмента до единой строки исходников. c.Status(code) в ctx.go объявлен как func (c *DefaultCtx) Status(status int) Ctx: ставит код, тело не трогает и возвращает Ctx — отсюда цепочка c.Status(fiber.StatusCreated).JSON(...), которую мы используем со времён восьмого урока. Это инструмент «код плюс тело одним выражением».

c.SendStatus(status) в res.go умнее по телу. Сначала он ставит код, затем спрашивает statusDisallowsBody: для 1xx, 204 No Content, 205 Reset Content и 304 Not Modified тело запрещено самим RFC — ResetBody сбрасывает его дочиста. Если статус тело разрешает, а тело пусто, SendStatus кладёт статусную фразу utils.StatusMessage(status): «Not Found», «Created», «Unprocessable Entity». Если тело уже записано — не трогает ни кодовую фразу, ни содержимое. Отсюда правило выбора: есть что сериализовать — c.Status(...).JSON(...); отвечаете только кодом — c.SendStatus(...), и пусть фраза и запреты тела решаются за вас.

Коды ответов JSON-API и константы Fiber#

КодКонстантаКогда ставить
200fiber.StatusOKуспешный GET: данные в теле, ничего создавать не пришлось
201fiber.StatusCreatedPOST создал ресурс: вернуть созданное с присвоенным ID
204fiber.StatusNoContentDELETE или обновление без тела в ответе; тело запрещено RFC
400fiber.StatusBadRequestзапрос сломан: тип поля не тот, JSON не парсится, поле пусто
422fiber.StatusUnprocessableEntityContent-Type не из списка Bind — тело нечем разобрать
500fiber.StatusInternalServerErrorошибка на нашей стороне; дефолтный код необработанных ошибок

POST с 201 и DELETE с 204

GO
// POST /api/tasks — создать: код 201 и созданная задача в теле
api.Post("/tasks", func(c fiber.Ctx) error {
        var in Task
        // разбор тела — тема второй половины урока
        if err := c.Bind().Body(&in); err != nil {
                return err
        }
        in.ID = nextID()
        in.CreatedAt = time.Now().Format(time.RFC3339)
        tasks = append(tasks, in)
        // цепочка: сначала код, потом JSON
        return c.Status(fiber.StatusCreated).JSON(in)
})

// DELETE /api/tasks — тела в ответе нет по RFC
api.Delete("/tasks", func(c fiber.Ctx) error {
        tasks = nil
        // SendStatus: код 204 и гарантированно пустое тело
        return c.SendStatus(fiber.StatusNoContent)
})

Здесь важны два кода. 201 Created — «ресурс создан, вот он»: клиенту не нужен второй GET, чтобы узнать ID и дату. 204 через SendStatus: статус запрещает тело, statusDisallowsBody вызывает ResetBody — и строка «No Content» в теле не уедет, хотя при 404 на пустом хендлере SendStatus положил бы в тело фразу Not Found. Вся семантика — RFC 9110, вшитая в res.go; константы StatusCreated и StatusNoContent живут в constants.go рядом с числами и ссылками на RFC.

Проверьте себя: что в теле у SendStatus

Хендлер завершается return c.SendStatus(fiber.StatusNotFound), и тело ответа пусто. Что получит клиент?

BodyParser уходит: c.Bind() в v3#

Ищете разбор JSON в Fiber — поисковик неизбежно приводит на слово BodyParser: так метод назывался в v2, и tutorials второго поколения построены на c.BodyParser(&in). Сверим с тегом v3.5.0: метода BodyParser на Ctx больше нет. На его месте — c.Bind(), и комментарий в ctx.go прямо называет замену: Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser. Один Bind собрал в себе весь разбор запроса.

Внутри — семейство методов. Тело: Body (диспетчер по Content-Type), JSON, XML, Form, MsgPack, CBOR. Прочие источники: Query — строка запроса, URI — параметры маршрута из третьего урока, Header и Cookie — заголовки и печенье, RespHeader — ответные заголовки. Все вместе сразу — All, свои биндеры — Custom. Практика перевода v2-примера в v3 механическая: c.BodyParser(&in) превращается в c.Bind().Body(&in), а поведение по Content-Type сохраняется. Поэтому запрос «bodyparser fiber» правильный, а ответ — Bind: tutorials не переведены, исходники — да.

Bind: источники данных и выбор парсера#

МетодИсточникКак выбирается
Body()тело запросадиспетчер по Content-Type: JSON, XML, формы, MsgPack, CBOR
JSON()тело запросавсегда JSON-декодер, заголовок Content-Type игнорирует
XML(), Form()тело запросаXML-документ; формы urlencoded и multipart с файлами
Query(), URI()строка запроса и путьпараметры URL и :id из маршрута — источники четвёртого и третьего уроков
Header(), Cookie()заголовки и cookieразбор по тегам структуры, включая X-API-Key стража
All()всё сразупорядок: URI, тело, query, заголовки, cookie — настраивается тегом binding_source

c.Bind().Body: диспетчер по Content-Type

GO
// POST /api/tasks: Content-Type решает, чем парсить тело
api.Post("/tasks", func(c fiber.Ctx) error {
        var in Task
        // application/json → JSON-декодер, формы → form-биндер
        if err := c.Bind().Body(&in); err != nil {
                // чужой Content-Type и битый JSON приходят сюда
                return c.Status(fiber.StatusBadRequest).JSON(
                        fiber.Map{"error": "запрос не разобран"},
                )
        }
        if in.Title == "" {
                return c.Status(fiber.StatusBadRequest).JSON(
                        fiber.Map{"error": "поле title обязательно"},
                )
        }
        in.ID = nextID()
        tasks = append(tasks, in)
        return c.Status(fiber.StatusCreated).JSON(in)
})

Механика из helpers.go: bindMediaType приводит заголовок к нижнему регистру и отбрасывает параметры после точки с запятой, поэтому «application/json; charset=utf-8» распознаётся как application/json; вендорные формы вида application/vnd.api+json сводятся к базовому типу. Дальше switch из bind.go: MIMEApplicationJSON уходит в Bind().JSON, формы — application/x-www-form-urlencoded и multipart/form-data — в Bind().Form, там же XML, MsgPack и CBOR. Ничего не подошло и кастомный биндер не нашёлся — возвращается ErrUnprocessableEntity, это *Error с кодом 422 из constants.go. Разница с Bind().JSON принципиальна: тот декодирует тело при любом заголовке и падает только на содержимом; декодер берётся из конфига — bind.go передаёт app.Config().JSONDecoder, дефолт которого json.Unmarshal назначается в app.go, так что подменить разбор JSON в проекте можно так же, как и кодировщик.

JSON с чужим Content-Type

Прочитайте код и выберите, что он выведет:

api.Post("/tasks", func(c fiber.Ctx) error {
        var in Task
        if err := c.Bind().Body(&in); err != nil {
                return err // ошибку отдаём как есть
        }
        return c.Status(fiber.StatusCreated).JSON(in)
})

// Запрос:
// POST /api/tasks
// Content-Type: text/plain
//
// {"title": "позвонить клиенту"}

Ошибки биндинга: BindError, UnmarshalTypeError и 400#

У разбора два режима, и дефолт — ручной. bindPool создаёт Bind с shouldSkipErrHandling: true: ошибка разбора возвращается как BindError, статус ответа НЕ ставится. BindError — структура из bind.go с тремя полями: Err — исходная ошибка, Source — источник из констант BindSource (body, query, uri, header, cookie), Field — имя поля, вычисленное по-умному: для JSON-ошибок *json.UnmarshalTypeError поле вытаскивается из самой ошибки, и строка «json: cannot unmarshal string into Go struct field Task.estimate of type int» превращается в estimate. Достать всё это — errors.As(err, &be).

Второй режим включает c.Bind().WithAutoHandling(): returnErr в bind.go ставит b.ctx.Status(StatusBadRequest) и возвращает NewError(StatusBadRequest, "Bad request: "+err.Error()) — готовый Error с кодом 400; return err вы всё равно пишете сами. И третья правда, о которой tutorials молчат: в ручном режиме вернули BindError как есть — для дефолтного ErrorHandler это не Error, поэтому код станет 500, а телом уедет err.Error() в text/plain. Клиент виноват, а получает Internal Server Error. Синтаксическая ошибка (битый JSON, json.SyntaxError) и несовпадение типов (json.UnmarshalTypeError) — обе проходят одной дорогой биндинга; различие — в сообщении и поле.

Свои JSON-ошибки на 400: errors.As из bind.go

GO
// POST /api/tasks — ручной режим: ошибки биндинга получают форму API
api.Post("/tasks", func(c fiber.Ctx) error {
        var in Task
        err := c.Bind().Body(&in)
        if err != nil {
                var be *fiber.BindError
                // errors.As достаёт источник и поле из ошибки биндинга
                if errors.As(err, &be) {
                        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                                "error":  "запрос не разобран",
                                "source": be.Source, // body
                                "field":  be.Field,  // estimate — из UnmarshalTypeError
                        })
                }
                return c.Status(fiber.StatusBadRequest).JSON(
                        fiber.Map{"error": "запрос не разобран"},
                )
        }
        if in.Title == "" {
                return c.Status(fiber.StatusBadRequest).JSON(
                        fiber.Map{"error": "поле title обязательно"},
                )
        }
        in.ID = nextID()
        tasks = append(tasks, in)
        return c.Status(fiber.StatusCreated).JSON(in)
})

Приём «страж пишет JSON» из седьмого урока сменил профессию: теперь единую форму {error, source, field} получают ошибки разбора. Клиенту не нужно гадать, JSON ли пришёл или текст ошибки — всё парсится одним кодом. errors.As вынимает *BindError: Source различит body, uri и query — полезно, когда к эндпоинту сходятся и путь, и тело; Field передаёт estimate прямо из UnmarshalTypeError. Альтернатива на стиле — bind := c.Bind().WithAutoHandling(): статус 400 и сообщение Bad request: ... проставятся сами, но уйдут как text/plain от ErrorHandler, а не вашим JSON. Два режима — две цены: меньше кода или полный контроль.

Проверьте себя: цена ручного режима

Bind в дефолтном ручном режиме. Хендлер сделал err := c.Bind().Body(&in) и вернул err как есть — без WithAutoHandling и без своих ответов. Клиент прислал строку в числовом поле. Что увидит клиент?

JSON-массив: батч-импорт и тело побольше#

Массив — тоже JSON-значение, и разбор идёт тем же методом: c.Bind().Body(&newTasks), где newTasks []Task, съест тело [{"title":"..."},{"title":"..."}] целиком. Практический сценарий — импорт: POST /api/tasks:import принимает список и добавляет всё сразу; ответ разумно вернуть в той же форме — {"items": [...]}, чтобы клиент получил обновлённый список одним вызовом. Разница nil и пустого среза видна и тут: тело [] даёт срез длины ноль, отсутствие ключа в объекте — нулевое значение поля, nil; с omitempty оба исчезнут из ответа, без него уедут как null и [] соответственно.

Два ограничения стоит знать заранее. Тело ограничено BodyLimit из fiber.Config — по умолчанию DefaultBodyLimit, четыре мебибайта; импорт миллионного массива режьте на страницы. А качество каждого элемента массива — на совести той же цепочки разбора: после декодирования bind.go вызывает validateStruct, который спрашивает config.StructValidator из fiber.Config. По умолчанию валидатор не задан — проверка молча пропускается, и задача «каждая задача имеет непустой title» остаётся вашей. Провода для валидации уже проложены, и это аккуратный мост к десятому уроку.

Каркас: структуры, коды, биндинг в одном main.go

GO
package main

import (
        "errors"
        "log"
        "time"

        "github.com/gofiber/fiber/v3"
        "github.com/gofiber/fiber/v3/middleware/cors"
        "github.com/gofiber/fiber/v3/middleware/helmet"
        "github.com/gofiber/fiber/v3/middleware/logger"
)

// Task — форма данных API: ключи и порядок задаёт тег json
type Task struct {
        ID        int    `json:"id"`
        Title     string `json:"title"`
        Done      bool   `json:"done"`
        Priority  string `json:"priority,omitempty"`
        Estimate  int    `json:"estimate,omitempty"`
        CreatedAt string `json:"created_at,omitempty"`
}

// страж из седьмого урока: без X-API-Key группа закрыта
func requireAPIKey(key string) fiber.Handler {
        return func(c fiber.Ctx) error {
                if c.Get("X-API-Key") != key {
                        return c.Status(fiber.StatusUnauthorized).JSON(
                                fiber.Map{"error": "неверный API-ключ"},
                        )
                }
                c.Locals("user", "spa-client")
                return c.Next()
        }
}

func main() {
        app := fiber.New()

        // 1-2: слои из восьмого урока — порядок прежний
        app.Use(helmet.New())
        app.Use(cors.New(cors.Config{
                AllowOrigins:     []string{"http://localhost:3000"},
                AllowHeaders:     []string{"Content-Type", "X-API-Key"},
                AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
                AllowCredentials: true,
                MaxAge:           3600,
        }))

        // 3: лог и защищённая группа
        app.Use(logger.New())
        api := app.Group("/api", requireAPIKey("s3cret"))

        // хранилище и счётчик — в памяти процесса
        tasks := make([]Task, 0)
        nextID := 0

        api.Get("/tasks", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"items": tasks})
        })

        api.Post("/tasks", func(c fiber.Ctx) error {
                var in Task
                // диспетчер по Content-Type: application/json → JSON
                if err := c.Bind().Body(&in); err != nil {
                        var be *fiber.BindError
                        if errors.As(err, &be) {
                                // свои JSON-ошибки вместо 500 с text/plain
                                return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                                        "error":  "запрос не разобран",
                                        "source": be.Source,
                                        "field":  be.Field,
                                })
                        }
                        return c.Status(fiber.StatusBadRequest).JSON(
                                fiber.Map{"error": "запрос не разобран"},
                        )
                }
                if in.Title == "" {
                        return c.Status(fiber.StatusBadRequest).JSON(
                                fiber.Map{"error": "поле title обязательно"},
                        )
                }
                nextID++
                in.ID = nextID
                in.CreatedAt = time.Now().Format(time.RFC3339)
                tasks = append(tasks, in)
                return c.Status(fiber.StatusCreated).JSON(in)
        })

        api.Delete("/tasks", func(c fiber.Ctx) error {
                tasks = nil
                return c.SendStatus(fiber.StatusNoContent)
        })

        // health-check вне группы и без ключа
        app.Get("/healthz", func(c fiber.Ctx) error {
                return c.JSON(fiber.Map{"ok": true})
        })

        log.Fatal(app.Listen(":8080"))
}

Каркас собирает урок в целое: слои восьмого урока не тронуты — шлем, CORS для SPA, лог, страж и /healthz; новое живёт внутри группы. Три эндпоинта — три кода из таблицы: 200 на список, 201 на создание с созданной задачей в теле, 204 на очистку через SendStatus. Разбор тела — ровно две ветки: ошибки биндинга с errors.As превращаются в {error, source, field} с кодом 400, дальше — своя проверка title. Если поле Title в вашем варианте помечено omitempty и без собственной проверки, пустая задача пройдёт — валидация полей и есть тема десятого урока.

Соберите жизненный цикл JSON-запроса

Расставьте шаги POST-запроса с JSON-телом по порядку: от команды в терминале до последнего заголовка ответа.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Допишите хендлер создания задачи

Вставьте пропуски: метод разбора, код для битого запроса, цепочка статуса и код создания ресурса.

api.Post("/tasks", func(c fiber.Ctx) error { var in Task // разбор тела: JSON по Content-Type if err := c.().Body(&in); err != nil { return c.Status(fiber.).JSON( fiber.Map{"error": "запрос не разобран"}, ) } in.ID = nextID() tasks = append(tasks, in) // код «ресурс создан» и созданная задача в ответе return c.(fiber.).JSON(in) })

Словарь урока

Термин

c.JSON

нажмите, чтобы перевернуть
Определение

Метод из res.go: JSONEncoder из конфига (дефолт json.Marshal), SetBodyRaw в тело и Content-Type application/json; charset=utf-8 — константа MIMEApplicationJSONCharsetUTF8. Второй аргумент ctype перекрывает заголовок.

1 / 6

Запуск, проверка curl-ом и что дальше#

Соберите каркас и прогоните четыре сценария. Первый — правильное создание: curl -i -X POST localhost:8080/api/tasks -H "Content-Type: application/json" -H "X-API-Key: s3cret" -d '{"title":"позвонить","priority":"high"}' — код 201, Content-Type: application/json; charset=utf-8 и созданная задача с id и created_at; priority в теле вернётся, estimate опустится по omitempty. Второй — ошибка типов: -d '{"title":"позвонить","estimate":"пять"}' — 400 и {"error":"запрос не разобран","source":"body","field":"estimate"}: Field приехал из UnmarshalTypeError, не нами придуман. Третий — чужой Content-Type: тот же JSON без -H "Content-Type: application/json" — 422 Unprocessable Entity от ErrUnprocessableEntity. Четвёртый — очистка: curl -i -X DELETE ... -H "X-API-Key: s3cret" — 204 и пустое тело.

Дальше по плану — десятый урок про валидацию: провода уже видели — config.StructValidator в fiber.Config и вызов validateStruct после разбора в bind.go, который сегодня молча пропускает проверку. Подключите валидатор — и «title обязателен» перестанет быть ручной проверкой. Обновить слои и стража — в восьмом уроке, фабрику слоёв и c.Locals — в седьмом, полный план — на странице курса Fiber. В браузерной песочнице сайта сетевые приложения не запускаются — runner-ов в уроках Fiber нет, проверяйте примеры в терминале с локальным Go.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • c.JSON из res.go делает три шага: JSONEncoder из конфига (дефолт json.Marshal), SetBodyRaw и Content-Type application/json; charset=utf-8 (MIMEApplicationJSONCharsetUTF8); второй аргумент перекрывает заголовок.
  • fiber.Map — type Map map[string]any для мелких ответов; ключи карт сортируются по алфавиту. Данные — структуры с тегами json: имена ключей, omitempty и json:"-" задаёт encoding/json, общий и для gin.
  • В Fiber v3 нет IndentedJSON, PureJSON, SecureJSON и AsciiJSON из gin: IndentedJSON заменяет подмена JSONEncoder в fiber.Config; экранирование <, >, & — общее с базовым gin. Взамен есть JSONP, MsgPack и CBOR.
  • Коды ответов: c.Status(code) ставит код и возвращает Ctx для цепочки .JSON(...); c.SendStatus(code) сбрасывает тело для 1xx/204/205/304 и пишет статусную фразу utils.StatusMessage при пустом теле. Канон: 200 на список, 201 на создание, 204 на удаление.
  • BodyParser из v2 заменён c.Bind(): Body — диспетчер по Content-Type (json/xml/form/msgpack/cbor, иначе ErrUnprocessableEntity 422), JSON — всегда JSON, плюс Query, URI, Header, Cookie и All (порядок URI → тело → query → заголовки → cookie).
  • Ошибки биндинга в ручном режиме — *BindError (Err, Source, Field из UnmarshalTypeError) без статуса: необработанная ошибка уезжает в 500 и text/plain. WithAutoHandling ставит 400 и Bad request: ...; контролируемый путь — errors.As + c.Status(400).JSON.
  • Батч-разбор массива идёт тем же Bind().Body в срез; тело ограничено BodyLimit (4 MiB по умолчанию); validateStruct после разбора спрашивает config.StructValidator и по умолчанию пропускается — мост к десятому уроку.

Домашнее задание

Развейте каркас: добавьте батч-импорт POST /api/tasks:import, который разбирает c.Bind().Body(&newTasks) в срез и возвращает {"items": [...]}; включите c.Bind().WithAutoHandling() в отдельном хендлере и сравните его ответ со своим JSON-вариантом из урока; подмените JSONEncoder на обёртку json.MarshalIndent и найдите отступы в ответе, затем верните дефолт; пришлите битый JSON (лишняя запятая) и убедитесь, что ошибка приходит по той же ветке errors.As с source body; наконец проверьте SendStatus(fiber.StatusResetContent) и объясните в комментарии, почему тело пусто.

Личные заметкиПусто
Хранятся только в вашем браузере (Fiber · Работа с JSON)

Вопросы и ответы по уроку

Почему у меня не работает c.BodyParser — он же есть во всех туториалах?

BodyParser — API второго поколения Fiber. В v3 на его месте метод c.Bind(): комментарий Replacement of: BodyParser, ParamsParser... в ctx.go называет его прямой заменой. Перевод механический: c.BodyParser(&in) → c.Bind().Body(&in), а точечные методы — c.Bind().JSON, c.Bind().Query, c.Bind().URI — покрывают бывшие ParamsParser и QueryParser.

Чем 422 отличается от 400 при разборе JSON?

422 (StatusUnprocessableEntity, ErrUnprocessableEntity) — когда Bind().Body не нашёл парсер: Content-Type не входит в список json/xml/form/msgpack/cbor. 400 (StatusBadRequest) — когда парсер найден, но тело не разобралось: битый JSON, несовпадение типов (UnmarshalTypeError), пустые обязательные поля. Первое лечится заголовком, второе — телом запроса.

Как получить JSON с отступами, как IndentedJSON в gin?

Отдельного метода в v3 нет — подмените кодировщик: fiber.New(fiber.Config{JSONEncoder: func(v any) ([]byte, error) { return json.MarshalIndent(v, "", " ") }}). Все c.JSON начнут отдавать с отступами. Для локальной отладки дешевле curl | jq, а в проде красивая печать — лишние байты и процессорное время на каждый ответ.

Термины урока

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Gin25 уроков~13 чпрактика25 уроков уже доступны

Gin: самый популярный веб-фреймворк Go

Курс Gin для Go: маршрутизация, binding, валидация, middleware, JWT, WebSocket, тесты и деплой. 25 уроков с кодом и итоговым REST API.

  • Уверенно владеть gin.Engine: маршруты, группы, версии
  • Привязывать запросы к структурам (ShouldBindJSON и др.)
  • Писать middleware и использовать встроенные
  • Строить аутентификацию на JWT