Go oraz ekosystem OpenAPI
Wstęp
Podczas tworzenia serwera backendowego w języku Go, niemal każdy programista na początku napotyka na jedno z następujących wyzwań:
Jak udokumentować API...?
Po krótkim poszukiwaniu można szybko zdać sobie sprawę, że korzystne jest tworzenie dokumentacji zgodnej ze specyfikacją OpenAPI i naturalnie zaczyna się poszukiwanie biblioteki współpracującej z OpenAPI. Jednak nawet po podjęciu takiej decyzji, pojawia się kolejne pytanie:
Jest tyle bibliotek związanych z OpenAPI... Którą wybrać...?
Ten dokument jest krótkim wprowadzeniem do bibliotek, napisanym dla początkujących użytkowników języka Go, którzy napotykają na te same trudności. Został on napisany pod koniec 2024 roku, a ponieważ ekosystem językowy jest dynamiczny, zalecamy korzystanie z niego z uwzględnieniem najnowszych informacji.
Strategie bibliotek w podejściu do OpenAPI
Jak już wiadomo, OpenAPI to specyfikacja służąca do precyzyjnego definiowania i dokumentowania interfejsów REST API. Definiuje ona punkty końcowe API, formaty żądań i odpowiedzi w formacie YAML lub JSON, co pomaga nie tylko programistom, ale także zespołom frontendowym i backendowym w automatyzacji generowania kodu, redukując bezsensowne powtórzenia i przyczyniając się do zmniejszenia drobnych błędów ludzkich.
W celu naturalnego zintegrowania OpenAPI z projektami, biblioteki w ekosystemie Go stosują głównie trzy strategie:
1. Łączenie adnotacji Go w dokument specyfikacji OpenAPI
Jednym z trudniejszych aspektów tworzenia API zgodnie z OpenAPI jest fakt, że dokumentacja i kod implementujący tę dokumentację istnieją w oddzielnych plikach w różnych lokalizacjach. W efekcie, aktualizacja kodu bez aktualizacji dokumentacji lub aktualizacja dokumentacji bez aktualizacji kodu jest zaskakująco częsta.
Przykładowo:
- Modyfikujesz logikę API w pliku
./internal/server/user.go
. - Rzeczywista dokumentacja znajduje się w
./openapi3.yaml
i łatwo można zapomnieć o jej zmianie. - Jeśli nie zauważysz tych zmian i wyślesz Pull Request do recenzji,
- Recenzenci również nie zauważą zmian w
./openapi3.yaml
. W rezultacie może dojść do niezgodności między specyfikacją API a rzeczywistą implementacją.
Tworzenie dokumentacji API w formie adnotacji Go może w pewnym stopniu rozwiązać ten problem. Ponieważ kod i dokumentacja znajdują się w jednym miejscu, możesz aktualizować adnotacje wraz z kodem. Istnieją narzędzia, które automatycznie generują dokument specyfikacji OpenAPI na podstawie tych adnotacji.
Reprezentatywnym projektem jest Swag. Swag analizuje adnotacje w kodzie Go i generuje dokument w formacie OpenAPI 2. Sposób użycia jest prosty: wystarczy umieścić adnotacje w odpowiednim formacie nad funkcją obsługi.
1// @Summary Utwórz użytkownika
2// @Description Tworzy nowego użytkownika.
3// @Tags Users
4// @Accept json
5// @Produce json
6// @Param user body models.User true "Informacje o użytkowniku"
7// @Success 200 {object} models.User
8// @Failure 400 {object} models.ErrorResponse
9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11 // ...
12}
Po dodaniu takich adnotacji, narzędzie CLI Swag przetworzy je i wygeneruje dokument OpenAPI 2. Zazwyczaj proces ten odbywa się w ramach CI, a wygenerowana dokumentacja specyfikacji OpenAPI jest wdrażana w repozytorium Git, końcowym produkcie kompilacji lub oddzielnym zewnętrznym systemie zarządzania dokumentacją API, gdzie jest wykorzystywana do współpracy z innymi projektami.
Zalety:
- Ponieważ adnotacje znajdują się obok kodu, zmniejsza się prawdopodobieństwo rozbieżności między kodem a dokumentacją.
- Możliwość łatwego i swobodnego tworzenia dokumentacji za pomocą adnotacji bez konieczności używania dodatkowych narzędzi czy skomplikowanej konfiguracji.
- Ponieważ adnotacje nie wpływają na logikę API, można dodawać tymczasowe funkcje, które nie powinny być publicznie udostępniane, bez obaw.
Wady:
- Duża liczba linii adnotacji może obniżyć czytelność pojedynczego pliku kodu.
- Trudności z wyrażeniem całej specyfikacji API za pomocą formy adnotacji.
- Ponieważ dokumentacja nie wymusza struktury kodu, nie ma gwarancji zgodności między dokumentacją OpenAPI a rzeczywistą logiką.
2. Generowanie kodu Go na podstawie dokumentacji specyfikacji OpenAPI
Można również przyjąć podejście, w którym źródłem prawdy (Single Source of Truth, SSOT) nie jest kod Go, ale dokumentacja. Chodzi o zdefiniowanie specyfikacji OpenAPI, a następnie wygenerowanie kodu Go na podstawie tej definicji. Ponieważ specyfikacja API generuje kod, wymusza się kulturalne projektowanie API na początku, a definicja specyfikacji API jest pierwszym krokiem procesu rozwoju, co pozwala na wcześniejsze zapobieganie sytuacjom, w których błędy są wykrywane dopiero po zakończeniu rozwoju i cały kod musi zostać zmodyfikowany wraz ze zmianą specyfikacji API.
Reprezentatywnymi projektami wykorzystującymi to podejście są oapi-codegen i OpenAPI Generator. Sposób użycia jest prosty:
- Tworzysz dokument yaml lub json zgodnie ze specyfikacją OpenAPI,
- Uruchamiasz CLI,
- Generowany jest odpowiedni kod szkieletowy Go.
- Wystarczy zaimplementować szczegółową logikę dla poszczególnych API, tak aby ten szkielet był użyteczny.
Poniżej znajduje się przykład kodu wygenerowanego przez oapi-codegen:
1// StrictServerInterface reprezentuje wszystkie procedury obsługi serwera.
2type StrictServerInterface interface {
3 // ...
4 // Zwraca wszystkie zwierzęta domowe
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
Poprzez ten interfejs, kod wygenerowany przez oapi-codegen wykonuje logikę analizy parametrów zapytania, nagłówków, treści i walidacji, a następnie wywołuje odpowiednią metodę zadeklarowaną w interfejsie. Użytkownik musi jedynie zaimplementować interfejs, aby ukończyć prace związane z implementacją API.
Zalety:
- Ponieważ specyfikacja jest opracowywana przed kodem, łatwiej jest prowadzić równoległą pracę w przypadku współpracy wielu zespołów.
- Kod dla powtarzalnych zadań jest generowany automatycznie, co zwiększa efektywność pracy, a jednocześnie ułatwia debugowanie.
- Łatwiej jest zapewnić zgodność dokumentacji i kodu.
Wady:
- Wymaga nauki specyfikacji OpenAPI, co może wiązać się z początkową trudnością.
- Ponieważ struktura kodu obsługującego API jest generowana automatycznie przez projekt, trudniej jest dostosować ją do własnych potrzeb.
Komentarz autora. W październiku 2024 roku, kod Go wygenerowany przez OpenAPI Generator wymusza strukturę całego projektu, a nie tylko logikę API, przez co struktura projektu jest sztywna i generuje kod, który jest nieodpowiedni do dodawania różnych funkcji potrzebnych w środowisku produkcyjnym. Jeśli wybierasz to podejście, zdecydowanie zalecam korzystanie z oapi-codegen. Autor używa oapi-codege + echo + StrictServerInterface.
3. Generowanie dokumentu specyfikacji OpenAPI za pomocą kodu Go
Gdy dziesiątki lub setki osób pracują nad tym samym serwerem, nieuniknione jest ryzyko utraty spójności między poszczególnymi API. Prostym przykładem jest sytuacja, w której specyfikacja ponad 100 punktów końcowych API jest zadeklarowana w jednym pliku OpenAPI yaml, gdzie taki plik staje się potworem z ponad 10 000 liniami. W takiej sytuacji deklarowanie nowego punktu końcowego API często prowadzi do duplikowania tych samych modeli, pomijania niektórych pól lub tworzenia nazw ścieżek niezgodnych z konwencją. W ten sposób zaczyna się tracić spójność całego API.
Aby rozwiązać ten problem, można wyznaczyć właściciela do zarządzania plikiem OpenAPI yaml lub opracować Linter, który automatycznie wychwytuje problemy podczas procesu CI. Można też użyć języka Go do zdefiniowania języka specyficznego dla domeny (Domain-specific language, DSL), aby wymusić spójność wszystkich API.
Reprezentatywnym projektem, który wykorzystuje tę technikę, jest Kubernetes (który zbudował ją samodzielnie bez użycia oddzielnej biblioteki), ale można również użyć projektów takich jak go-restful i goa. Poniżej znajduje się przykład użycia 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})
Zapisanie kodu Go, który można skompilować, pozwala jednocześnie zdefiniować implementację i dokumentację dla API POST /users
.
Zalety:
- Ponieważ wszystko pochodzi z kodu, łatwo jest zachować spójność API w całym projekcie.
- Wykorzystanie systemu silnego typowania Go pozwala na uzyskanie dokładniejszej i niebudzącej wątpliwości specyfikacji, niż przy pełnym wykorzystaniu wszystkich funkcji OpenAPI3.
Wady:
- Wymaga nauki DSL zdefiniowanego przez każdą platformę i trudno go zastosować do istniejącego kodu.
- Ponieważ reguły proponowane przez platformę muszą być bezwzględnie przestrzegane, swoboda i elastyczność mogą być ograniczone.
Podsumowanie
Każde z tych rozwiązań ma swoje zalety i wady, a wybór odpowiedniej metody zależy od wymagań projektu i preferencji zespołu. Najważniejsze jest to, aby ocenić, które rozwiązanie jest najbardziej odpowiednie do danej sytuacji, a nie które podejście jest lepsze, oraz skupić się na zwiększeniu produktywności programistycznej, aby szybko zakończyć pracę i cieszyć się satysfakcjonującym balansem między życiem zawodowym a prywatnym.
Chociaż artykuł ten został napisany w październiku 2024 roku, ekosystem Go i OpenAPI stale się rozwija, dlatego też, biorąc pod uwagę upływ czasu, zachęcamy do śledzenia najnowszych informacji o bibliotekach i projektach oraz ich zmieniających się zaletach i wadach.
Życzymy udanego życia z Go! 😘