Go og OpenAPI økosystem
Innledning
Når man utvikler produksjons-backend-servere i Go, er en av de første utfordringene nesten alle utviklere møter som følger:
API-dokumentasjon, hvordan skal jeg gjøre det...?
Ved å undersøke dette noe, innser man at det er fordelaktig å utarbeide dokumentasjon som samsvarer med OpenAPI-spesifikasjonen, og man begynner naturligvis å søke etter biblioteker som integreres med OpenAPI. Men selv etter å ha tatt denne beslutningen, eksisterer det et etterfølgende problem.
Det er mange OpenAPI-relaterte biblioteker... Hvilket bør jeg bruke...?
Dette dokumentet er en kort introduksjon til biblioteker, utarbeidet for Go-nybegynnere som opplever slike situasjoner. Dette dokumentet er utarbeidet per slutten av 2024, og ettersom språkøkosystemet er i konstant flyt, anbefales det å konsultere det samtidig som man kontinuerlig følger med på den nyeste utviklingen.
Strategier for Biblioteker som Forholder seg til OpenAPI
Selv om det kanskje er kjent fra før, er OpenAPI en spesifikasjon for å tydelig definere og dokumentere REST APIer. Ved å definere API-endepunkter, forespørsler, svarformater osv. i YAML- eller JSON-format, automatiseres kodeutviklingen for frontend så vel som backend, noe som bidrar betydelig til å redusere meningsløs repetisjon og små menneskelige feil.
For å integrere denne OpenAPI-en naturlig med prosjektet, benytter bibliotekene i Go-økosystemet hovedsakelig følgende tre strategier.
1. Kombinere Go-kommentarer til OpenAPI Spesifikasjonsdokument
Et av de vanskelige punktene ved å utvikle APIer i henhold til OpenAPI, er at den faktiske dokumentasjonen og koden som implementerer denne dokumentasjonen eksisterer i separate filer på vidt forskjellige steder, noe som fører til at situasjoner der man oppdaterer koden, men ikke dokumentasjonen, eller oppdaterer dokumentasjonen, men ikke koden, oppstår oftere enn man skulle tro.
Et enkelt eksempel illustrerer dette:
- API-logikken i filen
./internal/server/user.go
ble modifisert, og - Den faktiske dokumentasjonen eksisterer i
./openapi3.yaml
, og man kan ved et uhell glemme å oppdatere denne. - Dersom man sender inn en Pull Request uten å være klar over disse endringene og får den vurdert av kolleger,
- kan det oppstå den uheldige situasjonen at API-spesifikasjonen forblir uendret mens den faktiske API-implementasjonen har endret seg, ettersom anmelderne heller ikke ser endringene i
./openapi3.yaml
.
Dersom API-dokumentasjonen skrives i form av Go-kommentarer, kan dette problemet lindres til en viss grad. Siden koden og dokumentasjonen er samlet på ett sted, kan man oppdatere kommentarene samtidig som man modifiserer koden. Det eksisterer verktøy som automatisk genererer OpenAPI-spesifikasjonsdokumenter basert på slike kommentarer.
Et fremtredende prosjekt i denne sammenhengen er Swag. Swag parser kommentarene i Go-koden for å generere dokumentasjon i OpenAPI 2-format. Bruksmåten er enkel. Man behøver kun å skrive kommentarer over handler-funksjonen i henhold til formatet definert av det respektive biblioteket.
1// @Summary Opprett bruker
2// @Description Oppretter en ny bruker.
3// @Tags Brukere
4// @Accept json
5// @Produce json
6// @Param user body models.User true "Brukerinformasjon"
7// @Success 200 {object} models.User
8// @Failure 400 {object} models.ErrorResponse
9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11 // ...
12}
Når kommentarer skrives på denne måten, parser CLI-verktøyet kalt Swag disse kommentarene for å generere OpenAPI 2-dokumentasjonen. Vanligvis utføres denne prosessen i CI-flyten, og det genererte OpenAPI-spesifikasjonsdokumentet distribueres til Git Repository, sluttresultatet av byggingen, eller et separat eksternt API-dokumenthåndteringssystem, for bruk i samarbeid med andre prosjekter.
Fordeler:
- Siden kommentarene er sammen med koden, reduseres sannsynligheten for at den faktiske koden og dokumentasjonen avviker i form.
- Det er mulig å dokumentere enkelt og fritt kun ved hjelp av kommentarer, uten behov for separate verktøy eller komplekse konfigurasjoner.
- Ettersom kommentarene ikke påvirker selve API-logikken, er det gunstig for å legge til midlertidige funksjoner som det er ubehagelig å publisere i dokumentasjonen.
Ulemper:
- Ettersom antall kommentarlinjer øker, kan lesbarheten til enkeltkodefiler reduseres.
- Det kan være vanskelig å uttrykke hele API-spesifikasjonen i form av kommentarer.
- Siden dokumentasjonen ikke tvinger koden, kan det ikke garanteres at OpenAPI-dokumentet og den faktiske logikken stemmer overens.
2. Generere Go-kode fra OpenAPI Spesifikasjonsdokumentet
Det finnes også en metode der Single source of Truth (SSOT) plasseres i dokumentasjonen, snarere enn i Go-koden. Dette er en metode der man først definerer OpenAPI-spesifikasjonen og deretter genererer Go-koden basert på det definerte innholdet. Fordi API-spesifikasjonen genererer koden direkte, kan det påtvinge en utviklingskultur der API-design prioriteres, og siden definisjonen av API-spesifikasjonen er det aller første trinnet i utviklingssekvensen, har den den fordelen at uheldige hendelser der man først oppdager manglende deler etter fullført utvikling, og må modifisere hele koden sammen med API-spesifikasjonen, kan forhindres tidlig.
Fremtredende prosjekter som benytter denne tilnærmingen inkluderer oapi-codegen og OpenAPI Generator. Bruksmåten er enkel.
- Man utarbeider et yaml- eller json-dokument som samsvarer med OpenAPI-spesifikasjonen, og
- deretter kjører man CLI-verktøyet,
- hvorpå tilsvarende Go stub-kode genereres.
- Deretter er det kun nødvendig å implementere detaljlogikken for de individuelle APIene slik at denne stuben kan benyttes.
Følgende er et eksempel på kode generert av oapi-codegen.
1// StrictServerInterface representerer alle server-handlere.
2type StrictServerInterface interface {
3 // ...
4 // Returnerer alle kjæledyr
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
Via grensesnittet ovenfor utfører koden generert av oapi-codegen logikk for parsing og validering av query parameters, headere og kropp, og er strukturert slik at den kaller den passende metoden deklarert i grensesnittet. Brukeren trenger kun å implementere en implementasjon av grensesnittet ovenfor, og arbeidet som kreves for API-implementasjonen er da fullført.
Fordeler:
- Ettersom spesifikasjonen kommer først og deretter utviklingen, er det fordelaktig for parallell arbeidsutførelse ved samarbeid mellom flere team.
- Fordi koden for de delene som tidligere ble utført som repetitivt, rutinemessig arbeid blir automatisk generert, øker arbeidseffektiviteten, samtidig som det fortsatt er gunstig for feilsøking.
- Det er enkelt å garantere at spesifikasjonens form og koden alltid samsvarer.
Ulemper:
- Dersom man er uvitende om selve OpenAPI-spesifikasjonen, eksisterer det en viss innledende læringskurve.
- Ettersom formen på koden som håndterer APIet genereres automatisk av prosjektet, kan det være vanskelig å tilpasse seg dersom det kreves tilpasning.
Forfatterens kommentar. Go-koden generert av OpenAPI Generator per oktober 2024 tvinger ikke bare API-logikken, men også hele prosjektstrukturen, og siden prosjektstrukturen er rigide, genererer den en kodeform som er uegnet for å legge til de mangfoldige funksjonene som kreves i et reelt produksjonsmiljø. De som velger denne metoden, anbefales sterkt å benytte oapi-codegen. Forfatteren benytter oapi-codegen + echo + StrictServerInterface.
3. Generere OpenAPI Spesifikasjonsdokument fra Go-kode
Når titalls eller hundrevis av mennesker utvikler på den samme serveren, er det et uunngåelig problem at konsistensen på tvers av individuelle APIer kan brytes. Som et intuitivt eksempel, dersom spesifikasjonen for over hundre API-endepunkter deklareres i én enkelt OpenAPI yaml-fil, vil den filen ha blitt et monster på over 10 000 linjer, og i forbindelse med deklarering av nye API-endepunkter, vil den totale API-konsistensen begynne å brytes gjennom uunngåelige hendelser som å duplisere samme modell, utelate enkelte felt, eller skape Path-navngivning som ikke samsvarer med konvensjoner.
For å løse slike problemer, kan man utnevne en dedikert eier for å administrere OpenAPI yaml-filen, eller utvikle en Linter for å automatisk fange opp feil under CI-prosessen, men det er også mulig å tvinge gjennom en konsistent enhetlighet for alle APIer ved å definere et Domain-specific language (DSL) i Go-språket.
Kubernetes er et fremtredende prosjekt som anvender denne teknikken (selvbygd uten separate biblioteker), og man kan også eksperimentere med prosjekter som go-restful og goa. Følgende er et brukseksempel for 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})
Ved å skrive kompilerbar Go-kode som vist ovenfor, oppnår man den fordelen at implementasjonen for POST /users
APIet og definisjonen av dokumentasjonen fullføres samtidig.
Fordeler:
- Ettersom alt utledes fra koden, er det enkelt å opprettholde API-konsistens for hele prosjektet.
- Ved å utnytte Go’s sterke typesystem, kan man oppnå en mer presis og ubestridelig spesifikasjon enn ved å utnytte alle funksjonene i OpenAPI3.
Ulemper:
- Man må lære seg DSL-en definert av hvert rammeverk, og det kan være vanskelig å implementere på eksisterende kode.
- Siden man er tvunget til å følge reglene foreslått av rammeverket, kan friheten og fleksibiliteten bli redusert.
Avslutning
Hver metode har sine fordeler og ulemper, og det er viktig å velge den metoden som er mest egnet basert på prosjektets krav og teamets preferanser. Det viktigste er aldri hvilken metode som er best, men å foreta en verdivurdering av hva som er den mest hensiktsmessige løsningen for den nåværende situasjonen, for å oppnå høy utviklingsproduktivitet og nyte tidlig avslutning og tilfredsstillende balanse mellom arbeid og fritid.
Selv om dette innlegget ble skrevet per oktober 2024, utvikler Go- og OpenAPI-økosystemene seg kontinuerlig. Derfor, når man tar hensyn til tidsgapet siden denne teksten ble lest, bør man kontinuerlig følge opp statusen og de endrede fordeler/ulemper for hvert bibliotek og prosjekt.
Ha et lykkelig Go-liv~ 😘