GoSuda

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

By iwanhae views ...

Введение

Когда разработчики начинают создавать производственный backend-сервер на Go, почти каждый сталкивается с одной из первых сложных задач:

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

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

Библиотек для OpenAPI так много... Какую же выбрать...?

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

Стратегии библиотек в работе с OpenAPI

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

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

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

Одна из сложностей при разработке 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. Будет сгенерирован соответствующий stub-код на Go
  4. Теперь достаточно самостоятельно реализовать детальную логику для отдельных 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~ 😘