GoSuda

Go и экосистема OpenAPI

By iwanhae
views ...

Введение

При разработке Production Backend сервера на языке Go почти каждый разработчик сталкивается с одной из первых трудностей, которая формулируется следующим образом:

Как документировать API...?

Немного изучив этот вопрос, становится очевидным, что создание документации в соответствии со спецификацией OpenAPI выгодно, и естественным образом начинается поиск библиотек, интегрирующихся с OpenAPI. Однако, даже после принятия такого решения, возникает следующая проблема:

Существует много библиотек, связанных с OpenAPI... Какую из них использовать...?

Данный документ представляет собой краткий обзор библиотек, предназначенный для новичков в Go, столкнувшихся с подобной ситуацией. Документ составлен по состоянию на конец 2024 года, и поскольку экосистема языка постоянно меняется, рекомендуется использовать его как справочник, всегда отслеживая последние изменения.

Стратегии библиотек по отношению к OpenAPI

Как вы, возможно, уже знаете, OpenAPI — это спецификация для четкого определения и документирования REST API. Она позволяет описывать конечные точки API, форматы запросов и ответов в формате YAML или JSON, что помогает не только разработчикам, но и автоматизирует генерацию кода для фронтенда и бэкенда, сокращая бессмысленные повторения и уменьшая количество мелких человеческих ошибок.

Для естественной интеграции OpenAPI в проекты, библиотеки экосистемы Go используют три основные стратегии.

1. Комбинирование комментариев Go в документ спецификации OpenAPI

Одной из сложностей при разработке API в соответствии с OpenAPI является то, что фактический документ и код, реализующий его, существуют в отдельных файлах и совершенно разных местах. Из-за этого довольно часто возникают ситуации, когда код обновляется, но документация не обновляется, или наоборот, документация обновляется, но код остается без изменений.

Приведем простой пример:

  1. Если логика API изменяется в файле ./internal/server/user.go,
  2. но фактическая документация находится в ./openapi3.yaml, то можно случайно забыть внести соответствующие изменения.
  3. Если такой Pull Request отправляется на ревью коллегам без осознания этой проблемы,
  4. ревьюеры также не увидят изменений в ./openapi3.yaml, что может привести к неприятной ситуации, когда спецификация API остается прежней, но фактическая реализация API меняется.

Создание документации API в виде комментариев Go может в некоторой степени решить эту проблему. Поскольку код и документация находятся в одном месте, комментарии можно обновлять одновременно с изменением кода. Существуют инструменты, которые автоматически генерируют документ спецификации OpenAPI на основе таких комментариев.

Ярким примером такого проекта является Swag. Swag анализирует комментарии в Go коде и генерирует документ в формате OpenAPI 2. Использование его просто: достаточно написать комментарии над функцией-обработчиком в соответствии с форматом, определенным каждой библиотекой.

 1// @Summary Создать пользователя
 2// @Description Создает нового пользователя.
 3// @Tags Users
 4// @Accept json
 5// @Produce json
 6// @Param user body models.User true "Информация о пользователе"
 7// @Success 200 {object} models.User
 8// @Failure 400 {object} models.ErrorResponse
 9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11    // ...
12}

При написании таких комментариев CLI-инструмент Swag анализирует их и генерирует документ OpenAPI 2. Как правило, эта операция выполняется в процессе CI, а сгенерированный документ спецификации OpenAPI развертывается в Git-репозитории, в конечном результате сборки или в отдельной внешней системе управления документацией API для использования в сотрудничестве с другими проектами.

Преимущества:

  • Поскольку комментарии находятся вместе с кодом, вероятность расхождения между фактическим кодом и документацией уменьшается.
  • Документирование может быть выполнено просто и свободно только с помощью комментариев, без необходимости в отдельных инструментах или сложной настройке.
  • Поскольку комментарии не влияют на фактическую логику API, они хорошо подходят для добавления временных функций, которые неудобно публиковать в документации.

Недостатки:

  • Увеличение количества строк комментариев может снизить читабельность отдельного файла кода.
  • Выразить всю спецификацию API в форме комментариев может быть сложно.
  • Поскольку документация не принуждает к коду, невозможно гарантировать соответствие документа OpenAPI фактической логике.

2. Генерация Go кода из документа спецификации OpenAPI

Существует также подход, при котором "единый источник истины" (Single Source of Truth, SSOT) находится не в Go коде, а в документации. Это метод, при котором сначала определяется спецификация OpenAPI, а затем на основе этого определения генерируется Go код. Поскольку спецификация API сама генерирует код, это позволяет культурно принуждать к предварительному проектированию API, а также, поскольку определение спецификации API является первым шагом в последовательности разработки, это позволяет на ранней стадии предотвратить неприятные ситуации, когда упущенные детали осознаются только после завершения разработки, что приводит к изменению спецификации API и всей кодовой базы.

Примерами проектов, использующих этот подход, являются oapi-codegen и OpenAPI Generator. Использование их просто:

  1. Создайте YAML или JSON документ в соответствии со спецификацией OpenAPI.
  2. Запустите CLI.
  3. Будет сгенерирован соответствующий Go stub код.
  4. Теперь остается только реализовать детальную логику для каждого API, чтобы этот stub мог её использовать.

Ниже приведен пример кода, генерируемого oapi-codegen.

1// StrictServerInterface представляет все обработчики сервера.
2type StrictServerInterface interface {
3	// ...
4	// Возвращает всех питомцев
5	// (GET /pets)
6	FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7	// ...
8}

Сгенерированный oapi-codegen код на основе вышеуказанного интерфейса выполняет парсинг и валидацию query parameters, header, body и вызывает соответствующий метод, объявленный в интерфейсе. Пользователю достаточно реализовать только реализацию этого интерфейса, чтобы завершить работу по реализации API.

Преимущества:

  • Поскольку спецификация разрабатывается первой, а затем следует разработка, это облегчает параллельное выполнение задач при сотрудничестве нескольких команд.
  • Код для повторяющихся рутинных операций генерируется автоматически, что повышает эффективность работы и при этом остается удобным для отладки.
  • Легко гарантировать постоянное соответствие документации и кода.

Недостатки:

  • При отсутствии знаний о самой спецификации OpenAPI начальная кривая обучения может быть довольно крутой.
  • Поскольку форма кода, обрабатывающего API, генерируется проектом автоматически, могут возникнуть трудности при необходимости кастомизации.

Комментарий автора. По состоянию на октябрь 2024 года, Go-код, генерируемый OpenAPI Generator, не только принуждает к логике API, но и к общей структуре проекта, делая её жесткой. Это приводит к генерации кода, непригодного для добавления различных функций, необходимых в реальной Production среде. Тем, кто выбирает этот подход, настоятельно рекомендую использовать oapi-codegen. Автор использует oapi-codegen + echo + StrictServerInterface.

3. Генерация документа спецификации OpenAPI из Go кода

Когда десятки или сотни людей разрабатывают один и тот же сервер, неизбежно возникают проблемы с нарушением единообразия между отдельными API. В качестве наглядного примера, если спецификации для более чем 100 конечных точек API объявить в одном файле OpenAPI yaml, этот файл превратится в монстра, превышающего 10 000 строк. При объявлении новой конечной точки API неизбежно будут дублироваться одни и те же модели, пропускаться некоторые поля, или появляться названия Path, не соответствующие соглашениям, что приведет к нарушению общего единообразия API.

Для решения этой проблемы можно назначить отдельного владельца для управления OpenAPI yaml, или разработать Linter для автоматического выявления проблем в процессе CI. Однако, можно также определить Domain-specific language (DSL) на языке Go, чтобы принудительно обеспечить согласованность всех API.

Примером проекта, использующего этот метод, является Kubernetes (построенный самостоятельно без отдельной библиотеки), а также можно использовать такие проекты, как go-restful и goa. Ниже приведен пример использования goa.

 1var _ = Service("user", func() {
 2    Method("create", func() {
 3        Payload(UserPayload)
 4        Result(User)
 5        HTTP(func() {
 6            POST("/users")
 7            Response(StatusOK)
 8        })
 9    })
10})

Написание компилируемого Go кода, как показано выше, дает преимущество одновременной реализации API POST /users и определения документации.

Преимущества:

  • Поскольку все исходит из кода, легко поддерживать согласованность API для всего проекта.
  • Использование строго типизированной системы Go позволяет получить более точную и однозначную спецификацию, чем при использовании всех функций OpenAPI3.

Недостатки:

  • Необходимо изучать DSL, определенный в каждом фреймворке, и применение к существующему коду может быть затруднительным.
  • Поскольку правила, предложенные фреймворком, должны соблюдаться принудительно, снижается степень свободы и гибкости.

В заключение

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

Хотя этот текст был написан в октябре 2024 года, экосистема Go и OpenAPI постоянно развивается. Поэтому, учитывая временной интервал с момента прочтения этого текста, рекомендуется постоянно отслеживать актуальное состояние библиотек и проектов, а также их изменившиеся преимущества и недостатки.

Желаю вам счастливой Go-жизни! 😘