Go и OpenAPI екосистемата
Въведение
Когато се разработва Production Backend сървър с езика Go, едно от първите предизвикателства, с които се сблъскват почти всички разработчици, е следното:
Как да документираме API-то...?
Ако се направи малко проучване по този въпрос, става ясно, че е изгодно да се създава документация, съответстваща на спецификацията OpenAPI, и естествено се търсят библиотеки, които се интегрират с OpenAPI. Въпреки това, дори и след вземането на такова решение, възниква следващ проблем:
Има много библиотеки, свързани с OpenAPI... Коя да използвам...?
Този документ представлява кратко въведение в библиотеките, написано за начинаещи в Go, които преживяват подобна ситуация. Документът е написан към края на 2024 г. и тъй като екосистемата на езика постоянно се променя, препоръчително е да се използва като отправна точка, като винаги се следи за най-новите актуални събития.
Стратегии на библиотеките по отношение на OpenAPI
Както може би вече знаете, OpenAPI е спецификация за ясно дефиниране и документиране на REST API. Тя дефинира крайни точки, заявки, формати на отговори на API в YAML или JSON формат, което помага не само на разработчиците, но и автоматизира генерирането на код за frontend и backend, намалявайки безсмисленото повтаряне и минимизирайки дребните човешки грешки.
За да интегрират 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 Потребители
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 Repository, крайния резултат от компилацията или отделна външна система за управление на API документация, за да се използва за сътрудничество с други проекти.
Предимства:
- Тъй като коментарите са заедно с кода, вероятността действителният код и формата на документацията да се различават намалява.
- Документацията може да бъде лесно и свободно създадена само с коментари, без нужда от отделни инструменти или сложни настройки.
- Тъй като коментарите не влияят на действителната API логика, е добре да се добавят временни функции, които биха били твърде обременителни за публично документиране.
Недостатъци:
- С увеличаване на броя на редовете коментари, четливостта на един файл с код може да намалее.
- Може да е трудно да се изрази цялата API спецификация под формата на коментари.
- Тъй като документацията не налага кода, не може да се гарантира, че OpenAPI документацията и действителната логика са съвместими.
2. Генериране на Go код от OpenAPI спецификационен документ
Съществува и подход, при който единичният източник на истина (SSOT) е документацията, а не Go кодът. Това е метод, при който първо се дефинира OpenAPI спецификацията, а след това се генерира Go код въз основа на дефинираното съдържание. Тъй като спецификацията на API генерира кода, това може да наложи култура на разработка, при която API дизайнът е на първо място. Тъй като дефинирането на API спецификацията е първата стъпка в процеса на разработка, това има предимството да предотврати от самото начало нещастни случаи, при които пропуснати части се осъзнават едва след завършването на разработката, което води до промяна на спецификацията на API и преработка на целия код.
Представителни проекти, които приемат този подход, са oapi-codegen и OpenAPI Generator. Използването е просто.
- Създайте yaml или json документ, съответстващ на OpenAPI спецификацията,
- изпълнете CLI,
- и съответният Go stub код ще бъде генериран.
- Сега трябва само да имплементирате детайлната логика за отделните 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, използвайки горния интерфейс като посредник, изпълнява логика като анализ на параметри на заявки, хедъри и тяло, както и валидация, и извиква подходящия метод, деклариран в интерфейса. Потребителят трябва само да имплементира този интерфейс, за да завърши работата по имплементацията на 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, или да се разработи 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 код, който може да бъде компилиран, както е показано по-горе, можете да получите предимството, че имплементацията на POST /users API и дефиницията на документацията се завършват едновременно.
Предимства:
- Тъй като всичко произлиза от кода, лесно е да се поддържа последователност на API за целия проект.
- Чрез използване на силната система за типизиране на Go, може да се получи по-точна и безспорна спецификация, отколкото при използване на всички функции на OpenAPI3.
Недостатъци:
- Трябва да се научи DSL, дефиниран от всяка рамка, и може да е трудно да се приложи към съществуващ код.
- Тъй като трябва да се следват принудително правилата, предложени от рамката, свободата и гъвкавостта могат да намалеят.
В заключение
Всеки метод има своите предимства и недостатъци и е важно да изберете подходящия метод въз основа на изискванията на проекта и предпочитанията на екипа. Най-важното винаги е не кой метод е най-добър, а да се направи преценка кой е най-подходящото решение за текущата ситуация и да се увеличи производителността на разработката, за да се насладите на бързо приключване на работата и задоволителен баланс между работа и личен живот.
Въпреки че този текст е написан към октомври 2024 г., екосистемата на Go и OpenAPI непрекъснато се развива, затова е препоръчително да следите актуалното състояние на всяка библиотека и проект, както и техните променени предимства и недостатъци, като вземете предвид времевия интервал от момента на прочитане на този текст.
Желая ви щастлив Go живот~ 😘