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

Введение
Когда разработчики начинают создавать производственный backend-сервер на Go, почти каждый сталкивается с одной из первых сложных задач:
Как же документировать API...?
Немного поискав, вы понимаете, что выгодно писать документацию в соответствии со спецификацией OpenAPI, и естественным образом находите библиотеки, интегрированные с OpenAPI. Однако даже после принятия этого решения возникает следующая проблема:
Библиотек для OpenAPI так много... Какую же выбрать...?
Этот документ представляет собой краткое введение в библиотеки, написанное для новичков в Go, которые сталкиваются с подобной ситуацией. Документ составлен по состоянию на конец 2024 года, и поскольку экосистема языка постоянно меняется, рекомендуется использовать его как справочный материал и регулярно следить за актуальными новостями.
Стратегии библиотек в работе с OpenAPI
Вероятно, вам уже известно, что OpenAPI является спецификацией для четкого определения и документирования REST API. Определяя конечные точки API, форматы запросов и ответов в формате YAML или JSON, она помогает не только разработчикам, но и автоматизирует генерацию кода для фронтенда и бэкенда, что существенно сокращает бессмысленные повторения и уменьшает мелкие человеческие ошибки.
Чтобы естественным образом интегрировать OpenAPI в проект, библиотеки экосистемы Go, как правило, придерживаются следующих трех стратегий.
1. Компоновка спецификации OpenAPI из комментариев Go
Одна из сложностей при разработке 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
- Будет сгенерирован соответствующий stub-код на Go
- Теперь достаточно самостоятельно реализовать детальную логику для отдельных API, которую сможет использовать этот stub.
Ниже приведен пример кода, который генерирует oapi-codegen.
1// StrictServerInterface represents all server handlers.
2type StrictServerInterface interface {
3 // ...
4 // Returns all pets
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
Код, сгенерированный oapi-codegen на основе этого interface, выполняет разбор query-параметров, header и body, валидацию и вызывает соответствующий method, объявленный в interface. Пользователю достаточно реализовать интерфейс, и работа по реализации API будет завершена.
Преимущества:
- Поскольку спецификация создается первой, а разработка идет после, при совместной работе нескольких команд задачи удобно выполнять параллельно.
- Код, который раньше писался вручную в рамках **повторяющейся рутинной работы, генерируется автоматически, поэтому эффективность работы возрастает, при этом отладка по-прежнему удобна.
- Легко гарантировать, что документация и код всегда соответствуют друг другу.
Недостатки:
- При отсутствии знаний о самой спецификации OpenAPI существует определенная крутая кривая обучения.
- Поскольку структура кода, обрабатывающего API, генерируется автоматически в соответствии с проектом, может быть сложно реагировать на случаи, требующие кастомизации.
Комментарий автора. По состоянию на октябрь 2024 года код, генерируемый OpenAPI Generator для Go, навязывает не только логику API, но и структуру всего проекта; поскольку архитектура становится жесткой, такой код плохо подходит для добавления различных функций, необходимых в реальной производственной среде. Тем, кто выбирает этот подход, настоятельно рекомендуется использовать oapi-codegen. Автор использует oapi-codegen + echo + StrictServerInterface.
3. Генерация документа спецификации OpenAPI из кода Go
Когда десятки и сотни людей разрабатывают один и тот же сервер, неизбежно возникает проблема: единообразие отдельных API может нарушаться. Наглядный пример: если описание более чем 100 конечных точек API объявлять в одном YAML-файле OpenAPI, этот файл неизбежно превратится в монстра из более чем 10 000 строк, и при добавлении новых конечных точек API модели будут неизбежно объявляться повторно, некоторые поля будут пропущены, а пути могут получать имена, не соответствующие конвенциям, в результате чего общая согласованность API начнет нарушаться.
Для решения этой проблемы можно назначить отдельного Owner, отвечающего за YAML-файлы OpenAPI, или разработать 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, можно получить более точную спецификацию, не вызывающую споров, по сравнению с использованием всех возможностей OpenAPI 3.
Недостатки:
- Необходимо изучить DSL, определенный каждым фреймворком, и его может быть сложно применить к существующему коду.
- Поскольку необходимо строго следовать правилам, предложенным фреймворком, гибкость и свобода могут снижаться.
Заключение
У каждого метода есть свои преимущества и недостатки, и важно выбрать подходящий в соответствии с требованиями проекта и предпочтениями команды. Самое главное не то, какой способ лучше, а то, чтобы, оценив, какое решение наиболее подходит для вашей текущей ситуации, повысить продуктивность разработки и наслаждаться ранним уходом с работы и удовлетворительным балансом между работой и личной жизнью.
Хотя эта статья написана по состоянию на октябрь 2024 года, экосистемы Go и OpenAPI продолжают активно развиваться, поэтому, учитывая время, прошедшее с момента прочтения, рекомендуем и дальше следить за актуальным состоянием библиотек и проектов, а также за изменениями их преимуществ и недостатков.
Желаю счастливой жизни с Go~ 😘