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, выполняет такие операции, как разбор параметров запроса, заголовка, тела и валидацию, и вызывает соответствующий метод, объявленный в интерфейсе. Пользователю нужно только реализовать реализацию для этого интерфейса, и работа, необходимая для реализации API, будет завершена.

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

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

Недостатки:

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

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

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

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

Чтобы решить эти проблемы, можно назначить владельца, управляющего OpenAPI YAML, или разработать линтер для автоматического обнаружения ошибок в процессе 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! 😘