Go i ekosystem OpenAPI
서론
Podczas rozwijania Production Backend serwera w języku Go, jednym z pierwszych wyzwań, z jakim spotyka się większość programistów, jest następujące:
Jak dokumentować API...?
Po krótkim poszukiwaniu uświadamiamy sobie, że korzystne jest tworzenie dokumentacji zgodnej ze specyfikacją OpenAPI i naturalnie zaczynamy szukać bibliotek integrujących się z OpenAPI. Jednak nawet po podjęciu takiej decyzji pojawia się kolejny problem:
Jest wiele bibliotek związanych z OpenAPI... Której użyć...?
Niniejszy dokument jest krótkim wprowadzeniem do bibliotek, przeznaczonym dla początkujących programistów Go, którzy doświadczają tej sytuacji. Dokument został napisany na koniec 2024 roku, a ponieważ ekosystem językowy stale się zmienia, zaleca się odwoływanie się do niego, jednocześnie śledząc najnowsze aktualizacje.
Strategie bibliotek wobec OpenAPI
Jak Państwo zapewne wiedzą, OpenAPI to specyfikacja służąca do jasnego definiowania i dokumentowania REST API. Definiuje ona punkty końcowe API, żądania i formaty odpowiedzi w formacie YAML lub JSON, co pomaga nie tylko programistom, ale także automatyzuje generowanie kodu dla frontendu i backendu, redukując bezsensowne powtórzenia i drobne błędy ludzkie.
Aby naturalnie połączyć OpenAPI z projektem, biblioteki w ekosystemie Go przyjmują głównie trzy strategie:
1. Łączenie komentarzy Go w dokumentację specyfikacji OpenAPI
Jedną z trudności podczas rozwijania API zgodnie z OpenAPI jest to, że rzeczywista dokumentacja i kod implementujący tę dokumentację istnieją w oddzielnych plikach i w zupełnie innych miejscach, co często prowadzi do sytuacji, w której kod jest aktualizowany, ale dokumentacja nie, lub dokumentacja jest aktualizowana, ale kod nie.
Prosty przykład:
- W pliku
./internal/server/user.gomodyfikujemy logikę API, - a rzeczywista dokumentacja znajduje się w
./openapi3.yaml, o której zmianie można przypadkowo zapomnieć. - Jeśli Pull Request zostanie wysłany, a koledzy dokonają recenzji, nieświadomi problemów związanych z tymi zmianami,
- recenzenci również nie widzą zmian w
./openapi3.yaml, co może prowadzić do nieszczęśliwej sytuacji, w której specyfikacja API pozostaje taka sama, ale rzeczywista implementacja API ulega zmianie.
Pisanie dokumentacji API w formie komentarzy Go może w pewnym stopniu rozwiązać ten problem. Ponieważ kod i dokumentacja są zgromadzone w jednym miejscu, komentarze mogą być aktualizowane jednocześnie ze zmianami w kodzie. Istnieją narzędzia, które automatycznie generują dokumentację specyfikacji OpenAPI na podstawie tych komentarzy.
Reprezentatywnym projektem jest Swag. Swag analizuje komentarze w kodzie Go i generuje dokumentację w formacie OpenAPI 2. Użycie jest proste: wystarczy napisać komentarze nad funkcjami handlera zgodnie z formatem określonym przez daną 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 dokumentację OpenAPI 2. Zazwyczaj proces ten odbywa się w ramach CI, a wygenerowana dokumentacja specyfikacji OpenAPI jest dystrybuowana do repozytorium Git, ostatecznych wyników kompilacji lub oddzielnych zewnętrznych systemów zarządzania dokumentacją API, gdzie jest wykorzystywana do współpracy z innymi projektami.
Zalety:
- Ponieważ komentarze znajdują się wraz z kodem, prawdopodobieństwo rozbieżności między rzeczywistym kodem a dokumentacją jest mniejsze.
- Dokumentacja może być tworzona łatwo i swobodnie za pomocą samych komentarzy, bez potrzeby dodatkowych narzędzi czy skomplikowanych konfiguracji.
- Ponieważ komentarze nie wpływają na rzeczywistą logikę API, łatwo jest dodawać tymczasowe funkcje, których ujawnienie w dokumentacji byłoby kłopotliwe.
Wady:
- Wzrost liczby linii komentarzy może obniżyć czytelność pojedynczego pliku kodu.
- Wyrażenie wszystkich specyfikacji API w formie komentarzy może być trudne.
- Ponieważ dokumentacja nie wymusza kodu, nie ma gwarancji, że dokumentacja OpenAPI i rzeczywista logika są zgodne.
2. Generowanie kodu Go z dokumentu specyfikacji OpenAPI
Istnieje również metoda, w której Single source of Truth (SSOT) jest umieszczany po stronie dokumentacji, a nie kodu Go. Polega ona na wcześniejszym zdefiniowaniu specyfikacji OpenAPI, a następnie generowaniu kodu Go na podstawie tej zdefiniowanej treści. Ponieważ specyfikacja API generuje kod, kultura deweloperska może wymusić najpierw projektowanie API, a ponieważ definiowanie specyfikacji API jest pierwszym krokiem w kolejności tworzenia, ma to tę zaletę, że pozwala wcześnie zapobiec nieszczęśliwym przypadkom, w których niedopatrzenia są zauważane dopiero po zakończeniu rozwoju, co prowadzi do zmiany specyfikacji API i modyfikacji całego kodu.
Reprezentatywnymi projektami, które przyjmują to podejście, są oapi-codegen i OpenAPI Generator. Sposób użycia jest prosty.
- Należy napisać dokument yaml lub json zgodny ze specyfikacją OpenAPI,
- uruchomić CLI,
- a zostanie wygenerowany odpowiadający mu kod Go stub.
- Następnie wystarczy zaimplementować szczegółową logikę dla poszczególnych API, aby ten stub mógł być używany.
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 powyższego interfejsu wykonuje logikę, taką jak parsowanie 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ć tę implementację interfejsu, aby zakończyć pracę nad API.
Zalety:
- Ponieważ specyfikacja jest tworzona najpierw, a rozwój następuje później, jest to korzystne dla równoległego prowadzenia prac w przypadku współpracy wielu zespołów.
- Kod dla powtarzalnych, ręcznych zadań jest generowany automatycznie, co zwiększa efektywność pracy, a jednocześnie nadal jest korzystne dla debugowania.
- Łatwo jest zagwarantować, że dokumentacja i kształt kodu są zawsze zgodne.
Wady:
- Jeśli nie ma się wiedzy na temat samej specyfikacji OpenAPI, początkowa krzywa uczenia się może być nieco stroma.
- Ponieważ kształt kodu obsługującego API jest automatycznie generowany przez projekt, dostosowanie może być trudne, jeśli jest to wymagane.
Komentarz autora. Według stanu na październik 2024 r. kod Go generowany przez OpenAPI Generator narzuca nie tylko logikę API, ale także cały kształt projektu, a struktura projektu jest tak sztywna, że generuje kod nieodpowiedni do dodawania różnych funkcji wymaganych w środowisku produkcyjnym. Osoby przyjmujące to podejście są gorąco zachęcane do korzystania z oapi-codegen. Autor używa oapi-codege + echo + StrictServerInterface.
3. Generowanie dokumentu specyfikacji OpenAPI z kodu Go
Gdy setki ludzi rozwija ten sam serwer, nieuchronnie pojawia się problem braku spójności między poszczególnymi API. Intuicyjnym przykładem jest sytuacja, gdy specyfikacja ponad 100 punktów końcowych API jest deklarowana w jednym pliku OpenAPI yaml. Taki plik stanie się potworem liczącym ponad 10 000 linii, a deklarowanie nowych punktów końcowych API nieuchronnie doprowadzi do duplikowania tych samych modeli, pomijania niektórych pól lub tworzenia nazw ścieżek niezgodnych z konwencjami, co zniszczy ogólną spójność API.
Aby rozwiązać ten problem, można przypisać właściciela do zarządzania plikiem OpenAPI yaml lub opracować Linter, aby automatycznie wykrywać błędy podczas procesu CI. Można również zdefiniować Domain-specific language (DSL) w języku Go, aby wymusić spójność wszystkich API.
Reprezentatywnym projektem, który stosuje tę technikę, jest Kubernetes (zbudowany samodzielnie bez oddzielnej biblioteki), a także można 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})
Pisząc kompilowalny kod Go, jak pokazano powyżej, można uzyskać tę zaletę, że implementacja API POST /users i definicja dokumentacji są wykonywane jednocześnie.
Zalety:
- Ponieważ wszystko pochodzi z kodu, łatwo jest zachować spójność API w całym projekcie.
- Wykorzystując system silnego typowania Go, można uzyskać bardziej precyzyjną i jednoznaczną specyfikację niż przy użyciu wszystkich funkcji OpenAPI3.
Wady:
- Należy opanować DSL zdefiniowany w poszczególnych frameworkach, a zastosowanie go do istniejącego kodu może być trudne.
- Ponieważ należy bezwzględnie przestrzegać zasad proponowanych przez framework, swoboda i elastyczność mogą być ograniczone.
Podsumowanie
Każda z metod ma swoje zalety i wady, a wybór odpowiedniej zależy od wymagań projektu i preferencji zespołu. Najważniejsze jest nie to, która metoda jest najlepsza, ale ocena, które rozwiązanie jest najbardziej odpowiednie dla obecnej sytuacji, aby zwiększyć produktywność, co prowadzi do szybszego zakończenia pracy i zadowalającej równowagi między życiem zawodowym a prywatnym.
Chociaż ten artykuł został napisany w październiku 2024 roku, ekosystem Go i OpenAPI stale się rozwija, dlatego zaleca się ciągłe śledzenie aktualizacji i zmian w zaletach i wadach poszczególnych bibliotek i projektów, biorąc pod uwagę odstęp czasu od momentu czytania tego tekstu.
Życzę szczęśliwego życia z Go! 😘