Есть особый жанр боли, знакомый всем гоферам, которые хоть раз отдавали свою HTTP‑ручку наружу: документация. Не та, что «напишите README», а честная машиночитаемая спека, по которой фронтенд сгенерирует клиент, QA — коллекцию, а API Gateway — валидацию.
// CreateUser godoc// @Summary Create user// @Accept json// @Produce json// @Param request body CreateUserRequest true "user"// @Success 201 {object} models.User// @Failure 400 {object} ErrorResponse// @Router /api/v1/users [post]func (h *UserHandler) Create(c fiber.Ctx) error { ... }
Восемь строк дублирования на одну ручку. И самое обидное: через месяц кто‑то поменяет models.User на dto.UserResponse, а комментарий останется. Компилятор промолчит, линтер промолчит, спека соврёт. Документация, которая расходится с кодом — хуже, чем её отсутствие, потому что ей верят.
Мне хотелось до неприличия простого: зайти в корень существующего проекта, выполнить одну команду и получить openapi.yaml. Не переписывая хендлеры, не навешивая аннотации, не внедряя новый роутер, не добавляя ни единой строчки в рабочий код.
Так появился fibgen.
go install github.com/nyawave/fibgen/cmd/fibgen@latestfibgen ./... > openapi.yaml
Дальше — про то, почему это оказалось не так тривиально, как хотелось бы, и что из этого вышло.
Почему с Fiber всё сложно
Экосистема Go в последнее время неплохо решает задачу «типизированного API». Есть huma, есть fuego, есть spec‑first подход через oapi‑codegen. Все они работают, и работают хорошо. Но у них общее свойство: они хотят, чтобы вы писали код по‑другому. Хендлер становится обобщённой функцией с явными типами func(ctx, In) (Out, error), из которых спека выводится тривиально.
А теперь посмотрим на типичный хендлер Fiber:
func (h *UserHandler) Create(c fiber.Ctx) error
Это всё. Сигнатура не содержит ничего. Ни типа запроса, ни типа ответа. Они существуют исключительно как локальные переменные внутри тела:
var req CreateUserRequestif err := c.Bind().Body(&req); err != nil { ... }...return c.Status(fiber.StatusCreated).JSON(user)
Рантайм‑рефлексия здесь бесполезна: с точки зрения reflect все хендлеры проекта — один и тот же тип. Информация о типах не то чтобы спрятана — она просто находится в другом месте. Внутри тела функции.
Значит, туда и надо идти.
Статический анализ: go/packages + go/types
Идея простая, хотя реализация — не очень. Мы загружаем проект тем же тайпчекером, которым его собирает компилятор, и ходим по AST:
-
Находим регистрации маршрутов: app.Get(…), app.Post(…), Add(method, path,…), All(…).
-
Разворачиваем группы: app.Group(“/api/v1”).Group(“/users”) → префикс /api/v1/users. Причём префиксы надо резолвить и через переменные (v1:= app.Group(“/api/v1”)), и через роутеры, переданные в функцию параметром — потому что примерно все так и делают: func RegisterUserRoutes(r fiber.Router).
-
Резолвим сам хендлер до тела функции: именованная функция, method value контроллера (h.Create), инлайновое замыкание, переменная функционального типа.
-
Идём внутрь тела и собираем вызовы c.*.
-
Переводим найденные Go‑типы в JSON Schema через go/types.
Пункт 5 — это, неожиданно, самая объёмная часть. Потому что «взять структуру и сделать из неё схему» звучит просто ровно до момента, когда вы вспоминаете про: json‑теги, omitempty (который на самом деле управляет списком required), встроенные поля с промоушеном, указатели (это nullable — причём в 3.1 и 3.0 nullable выражается по‑разному), слайсы, мапы, []byte, time.Time, uuid.UUID, primitive.ObjectID, json.RawMessage, any, и коллизии имён между пакетами, когда в проекте живут одновременно models.User и dto.User.
Отдельно порадовало вот это: если в коде есть именованный тип и группа констант этого типа:
type Status stringconst ( StatusActive Status = "active" StatusBlocked Status = "blocked")
то это очевидным образом enum, и в схему он попадает именно так. Никаких аннотаций для этого не нужно, информация уже есть в коде. Собственно, это и есть главный тезис всей затеи.
Что удалось вытащить из тела хендлера
Запросы. Тело — c.BodyParser(&dto) для v2, c.Bind().Body(&dto) и .JSON(&dto) для v3. Query — как структурой (c.QueryParser, c.Bind().Query, с разворачиванием по тегам query:), так и поштучно (c.Query, c.QueryInt, c.QueryBool). Path‑параметры берутся из шаблона маршрута, но тип уточняется по коду: если в теле есть c.ParamsInt("id"), значит это integer, а не string. Заголовки — c.Get("X-Request-Id") и парсеры.
Кстати, про пути. Fiber использует свой синтаксис, который надо переводить в OpenAPI::id → {id},:id? → необязательный,:id<int> → параметр с типом, * и + → wildcards.
Ответы. c.JSON(x) — это 200 со схемой x. c.Status(code).JSON(x) — статус берётся из code, причём как из литерала 201, так и из fiber.StatusCreated. Плюс SendStatus, SendString, Redirect.
Два случая, на которые ушло неприлично много времени, но без них выхлоп выглядел бы бедно — Ad‑hoc объекты. Гоферы, пишущие на Fiber, обожают fiber.Map:
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
Формально это map[string]any, и честный вывод типа дал бы бесполезное object без свойств. Поэтому литералы разворачиваются в схему с конкретными полями, типы значений выводятся из выражений, вложенные литералы и $ref поддерживаются. То есть {"message": "ok", "data": user} превращается в объект с message: string и data: $ref User.
Несколько форм ответа на один статус. Классика: хендлер отдаёт короткий объект при?short=true и полный в остальных случаях. Оба варианта — 200. Раньше я бы просто взял первый попавшийся; сейчас альтернативы собираются в oneOf.
Про детерминированность вывода
Мелочь, о которой я не подумал сразу, а потом пришлось переделывать: если спека коммитится в репозиторий (а её стоит коммитить — тогда её ревьюят вместе с кодом, и «ой, я не заметил, что сломал контракт» перестаёт быть отговоркой), то порядок ключей в YAML должен быть стабильным.
Мапы в Go, как известно, ходят в произвольном порядке. Первая версия давала на одном и том же коде разный вывод от запуска к запуску, и диффы выглядели как полная перезапись файла. Пришлось завести отдельный тип — упорядоченную по вставке мапу — и рендерить всё через неё. Скучно, но без этого инструмент бесполезен в CI.
Честные ограничения
Статический анализ не исполняет код, поэтому границы у него есть, и я предпочту перечислить их сам, а не ждать, пока это сделают в комментариях:
-
Ответы через хелперы не отслеживаются. Если у вас
respondJSON(c, http.StatusOK, user)вместо прямогоc.JSON(user)— тип не восстановится. Анализируется только тело самого хендлера, межпроцедурного анализа пока нет. Это первое, что стоит сделать дальше. -
Динамические пути — если путь собирается из неконстантной строки, маршрут пропускается (с note: в stderr).
-
Хендлеры за DI‑контейнером или интерфейсом, которые невозможно статически свести к функции, попадают в спеку с дефолтным ответом 200.
-
Дженерики в типах ответа обрабатываются ограниченно.
-
Описаний и summary нет. И не будет какое то время — их неоткуда взять, если не читать комментарии. Это сознательный размен: либо ноль аннотаций, либо человекочитаемые описания. Я выбрал первое, а комментарии можно будет добавить и в будущем.
Всё, что анализатор не смог разрешить, пишется в stderr как note:… — то есть вы видите, где спека неполна, а не думаете, что всё отлично. Заглушается флагом ‑quiet.
Что в итоге
Спека, выведенная из кода, обладает одним свойством, которого принципиально нет у аннотаций: она не может протухнуть. Переименовали поле — оно переименовалось в схеме. Поменяли статус — поменялся статус. Забыли обновить документацию — нечего забывать.
Поддерживаются Fiber v2 и v3, OpenAPI 3.0 и 3.1, вывод в YAML или JSON. В репозитории лежат два примера‑проекта (на v2 и v3), на которых гоняются тесты — сгенерированные документы валидируются через kin‑openapi.
fibgen -dir ./myservice -o openapi.json --openapi-version 3.0 ./...
Проект молодой, поэтому наверняка есть паттерны регистрации маршрутов и биндинга, которые я не предусмотрел просто потому, что сам так не пишу. Если fibgen на вашем проекте выдал пустоту или ерунду — это интересный баг, заводите issue с минимальным примером. Самые полезные направления для контрибьюта, на мой взгляд: межпроцедурный анализ (проход в хелперы ответов), новые паттерны биндинга и расширение списка well‑known типов.
Репозиторий: github.com/nyawave/fibgen, лицензия MIT.
ссылка на оригинал статьи https://habr.com/ru/articles/1063084/