Go и экосистема OpenAPI
Введение
При разработке Production Backend сервера на языке Go почти каждый разработчик сталкивается с одной из первых трудностей, которая формулируется следующим образом:
Как документировать API...?
Немного изучив этот вопрос, становится очевидным, что создание документации в соответствии со спецификацией OpenAPI выгодно, и естественным образом начинается поиск библиотек, интегрирующихся с OpenAPI. Однако, даже после принятия такого решения, возникает следующая проблема:
Существует много библиотек, связанных с OpenAPI... Какую из них использовать...?
Данный документ представляет собой краткий обзор библиотек, предназначенный для новичков в Go, столкнувшихся с подобной ситуацией. Документ составлен по состоянию на конец 2024 года, и поскольку экосистема языка постоянно меняется, рекомендуется использовать его как справочник, всегда отслеживая последние изменения.
Стратегии библиотек по отношению к OpenAPI
Как вы, возможно, уже знаете, OpenAPI — это спецификация для четкого определения и документирования REST API. Она позволяет описывать конечные точки API, форматы запросов и ответов в формате YAML или JSON, что помогает не только разработчикам, но и автоматизирует генерацию кода для фронтенда и бэкенда, сокращая бессмысленные повторения и уменьшая количество мелких человеческих ошибок.
Для естественной интеграции OpenAPI в проекты, библиотеки экосистемы Go используют три основные стратегии.
1. Комбинирование комментариев Go в документ спецификации OpenAPI
Одной из сложностей при разработке API в соответствии с OpenAPI является то, что фактический документ и код, реализующий его, существуют в отдельных файлах и совершенно разных местах. Из-за этого довольно часто возникают ситуации, когда код обновляется, но документация не обновляется, или наоборот, документация обновляется, но код остается без изменений.
Приведем простой пример:
- Если логика API изменяется в файле
./internal/server/user.go, - но фактическая документация находится в
./openapi3.yaml, то можно случайно забыть внести соответствующие изменения. - Если такой Pull Request отправляется на ревью коллегам без осознания этой проблемы,
- ревьюеры также не увидят изменений в
./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. Использование их просто:
- Создайте YAML или JSON документ в соответствии со спецификацией OpenAPI.
- Запустите CLI.
- Будет сгенерирован соответствующий Go stub код.
- Теперь остается только реализовать детальную логику для каждого 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-жизни! 😘