GoSuda

Go i ekosystem OpenAPI

By iwanhae views ...

Wstęp

Podczas tworzenia produkcyjnego serwera backendowego w języku Go prawie każdy programista natrafia na jeden z pierwszych poważnych problemów, a mianowicie:

Dokumentacja API, jak to zrobić...?

Po krótkim researchu okazuje się, że warto tworzyć dokumentację zgodną ze specyfikacją OpenAPI, i naturalnie zaczynamy szukać biblioteki współpracującej z OpenAPI. Jednak nawet po podjęciu tej decyzji pojawia się kolejny problem.

Bibliotek związanych z OpenAPI jest bardzo dużo.. Której powinienem użyć...?

Ten dokument to krótki przegląd bibliotek, napisany z myślą o początkujących w Go, którzy doświadczają takiej sytuacji. Dokument został przygotowany według stanu na koniec 2024 roku. Ekosystem języka zmienia się nieustannie, dlatego zachęcamy do korzystania z niniejszego materiału wraz z bieżącym śledzeniem najnowszych informacji.

Strategie bibliotek wobec OpenAPI

Jak zapewne wiadomo, OpenAPI jest specyfikacją służącą do jednoznacznego definiowania i dokumentowania REST API. Definiując punkty końcowe API, żądania oraz formaty odpowiedzi w formacie YAML lub JSON, można nie tylko ułatwić pracę programistom, ale także zautomatyzować generowanie kodu po stronie frontendu i backendu, co pozwala ograniczyć bezsensowne powtórzenia i drobne błędy ludzkie.

Aby naturalnie połączyć OpenAPI z projektem, biblioteki w ekosystemie Go przyjmują w głównej mierze trzy strategie:

1. Składanie specyfikacji dokumentu OpenAPI z komentarzy Go

Trudność w pracy zgodnej z OpenAPI polega na tym, że rzeczywisty dokument i kod, który go implementuje, znajdują się w osobnych plikach, w zupełnie różnych lokalizacjach. Dość często zdarza się, że kod zostaje zaktualizowany, ale dokumentacja już nie, albo dokumentacja zostaje zmieniona, a kodu nie udaje się zaktualizować.

Prosty przykład:

  1. Zmieniamy logikę API w pliku ./internal/server/user.go
  2. Rzeczywisty dokument znajduje się w ./openapi3.yaml, a zmianę w nim można łatwo przeoczyć.
  3. Jeśli nie zauważymy tego problemu, wyślemy Pull Request i poprosimy kolegów o review.
  4. Recenzenci również nie zobaczą zmian w ./openapi3.yaml, więc może dojść do sytuacji, w której specyfikacja API pozostaje bez zmian, a faktyczna implementacja API się zmienia.

Zapisywanie dokumentacji API w formie komentarzy Go w znacznym stopniu rozwiązuje ten problem. Kod i dokumentacja znajdują się w jednym miejscu, dzięki czemu komentarze można aktualizować równocześnie z modyfikacją kodu. Istnieją narzędzia, które automatycznie generują specyfikację OpenAPI na podstawie tych komentarzy.

Reprezentatywnym projektem jest Swag. Swag analizuje komentarze w kodzie Go i generuje dokumentację w formacie OpenAPI 2. Sposób użycia jest prosty: wystarczy napisać komentarze nad funkcją handlera, zgodnie z formatem określonym przez bibliotekę.

 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}

Po napisaniu takich komentarzy narzędzie CLI o nazwie Swag analizuje je i generuje dokument OpenAPI 2. Zazwyczaj proces ten uruchamiany jest w ramach CI, a wygenerowany dokument OpenAPI jest publikowany w Git Repository, w finalnym artefakcie buildu lub w zewnętrznym systemie zarządzania dokumentacją API, aby mógł być wykorzystywany podczas współpracy z innymi projektami.

Zalety:

  • Ponieważ komentarze znajdują się obok kodu, zmniejsza się prawdopodobieństwo rozbieżności między rzeczywistym kodem a dokumentacją.
  • Dokumentowanie można prowadzić łatwo i swobodnie, wyłącznie za pomocą komentarzy, bez dodatkowych narzędzi i skomplikowanej konfiguracji.
  • Komentarze nie wpływają na rzeczywistą logikę API, dlatego dobrze nadają się do dodawania tymczasowych funkcji, których nie chcemy udostępniać w dokumentacji.

Wady:

  • Wraz ze wzrostem liczby linii komentarzy czytelność pojedynczego pliku z kodem może ulec pogorszeniu.
  • Wyrażenie wszystkich specyfikacji API wyłącznie w formie komentarzy może być trudne.
  • Dokumentacja nie wymusza zgodności z kodem, dlatego nie można zagwarantować, że dokument OpenAPI odpowiada rzeczywistej logice.

2. Generowanie kodu Go na podstawie dokumentu OpenAPI

Istnieje również podejście, w którym Single Source of Truth (SSOT) znajduje się nie w kodzie Go, lecz w dokumencie. Chodzi o to, aby najpierw zdefiniować specyfikację OpenAPI, a następnie na jej podstawie wygenerować kod Go. Ponieważ specyfikacja API generuje kod, można kulturowo wymusić projektowanie API jako pierwszy krok. Ponadto, ponieważ definiowanie specyfikacji rozpoczyna się na samym początku procesu deweloperskiego, można wcześnie wykryć pominięte elementy i uniknąć sytuacji, w której dopiero po zakończeniu prac okazuje się, że trzeba zmienić specyfikację API i w konsekwencji cały kod.

Reprezentatywnymi projektami stosującymi to podejście są oapi-codegen oraz OpenAPI Generator. Sposób użycia jest prosty:

  1. Tworzymy dokument YAML lub JSON zgodny ze specyfikacją OpenAPI
  2. Uruchamiamy CLI
  3. Generowany jest odpowiadający mu kod stub w Go
  4. Wystarczy zaimplementować szczegółową logikę poszczególnych API, z których może korzystać wygenerowany stub

Poniżej znajduje się przykład kodu generowanego przez 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}

Kod wygenerowany przez oapi-codegen na podstawie tego interfejsu wykonuje parsowanie query parameters, header i body oraz walidację, a następnie wywołuje odpowiednią metodę zadeklarowaną w interfejsie. Użytkownik musi jedynie zaimplementować ten interfejs, aby zakończyć pracę nad implementacją API.

Zalety:

  • Ponieważ specyfikacja powstaje jako pierwsza, a rozwój przebiega później, w pracy wielu zespołów łatwiej prowadzić prace równolegle.
  • Kod, który wcześniej powstawał w ramach powtarzalnej, żmudnej pracy, jest generowany automatycznie, dzięki czemu wydajność pracy rośnie, a debugowanie nadal pozostaje łatwe.
  • Łatwo zapewnić, że dokumentacja i kod zawsze są zgodne ze sobą.

Wady:

  • Osoby, które nie znają specyfikacji OpenAPI, mogą napotkać stosunkowo strome początkowe krzywe uczenia się.
  • Ponieważ struktura kodu obsługującego API jest generowana automatycznie przez narzędzie, może być trudno dostosować ją do specyficznych potrzeb.

Komentarz autora. Według stanu na październik 2024 roku kod Go generowany przez OpenAPI Generator narzuca nie tylko logikę API, ale także całą strukturę projektu. Jest ona sztywna, przez co słabo nadaje się do dodawania wielu funkcji wymaganych w środowisku produkcyjnym. Osobom wybierającym to podejście zdecydowanie polecam oapi-codegen. Autor korzysta z kombinacji oapi-codegen + echo + StrictServerInterface.

3. Generowanie dokumentu OpenAPI na podstawie kodu Go

Gdy dziesiątki lub setki osób rozwijają ten sam serwer, nieuchronnie pojawia się problem, polegający na tym, że ujednolicenie poszczególnych API może zostać zaburzone. Dla przykładu: jeśli specyfikacja ponad 100 punktów końcowych API zostanie zadeklarowana w jednym pliku OpenAPI YAML, plik ten urośnie do potwora liczącego ponad 10 000 linii. Przy dodawaniu nowych punktów końcowych nieuchronnie zaczną się pojawiać zduplikowane definicje tych samych modeli, pominięte pola lub nazwy ścieżek niezgodne z konwencją, co stopniowo narusza spójność całego API.

Aby rozwiązać ten problem, można wyznaczyć osobę odpowiedzialną za plik OpenAPI YAML lub stworzyć linter wykrywający błędy automatycznie w CI. Można też jednak zdefiniować Domain-specific language (DSL) w języku Go, który wymusi spójność wszystkich API.

Reprezentatywnym projektem stosującym tę technikę jest Kubernetes (który buduje ją samodzielnie, bez zewnętrznych bibliotek). Można również wykorzystać takie projekty jak go-restful czy goa. Poniżej 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})

Pisząc kod Go, który się kompiluje, w ten sposób jednocześnie definiujemy implementację API POST /users oraz jego dokumentację, co jest dużą zaletą.

Zalety:

  • Ponieważ wszystko wynika z kodu, łatwiej utrzymać spójność API w całym projekcie.
  • Dzięki silnemu systemowi typów Go można uzyskać dokładniejszą i bardziej jednoznaczną specyfikację niż przy wykorzystaniu wszystkich funkcji OpenAPI 3.

Wady:

  • Trzeba poznać DSL zdefiniowany przez dany framework, a zastosowanie go do istniejącego kodu może być trudne.
  • Ponieważ trzeba bezwzględnie stosować się do reguł zaproponowanych przez framework, swoboda i elastyczność mogą być ograniczone.

Podsumowanie

Każda metoda ma swoje zalety i wady, dlatego ważne jest, aby wybrać rozwiązanie odpowiednie do wymagań projektu i preferencji zespołu. Najważniejsze jest nie to, która metoda jest lepsza, lecz to, by ocenić, jakie rozwiązanie najlepiej odpowiada aktualnej sytuacji, oraz zwiększać produktywność, aby móc wcześniej kończyć pracę i cieszyć się dobrym balansem między życiem zawodowym a prywatnym.

Choć artykuł został napisany według stanu na październik 2024 roku, ekosystemy Go i OpenAPI stale się rozwijają, dlatego warto na bieżąco śledzić aktualności oraz zmieniające się zalety i wady poszczególnych bibliotek i projektów, biorąc pod uwagę czas, jaki upłynął od publikacji.

Życzę udanej przygody z Go 😘