GoSuda

Go og OpenAPI-økosystemet

By iwanhae
views ...

Innledning

Når man utvikler Production Backend-servere i Go-språket, er en av de første utfordringene de fleste utviklere møter, ofte følgende:

API-dokumentasjon, hvordan gjør vi det?

En kort undersøkelse av dette vil avsløre at det er fordelaktig å skrive dokumentasjon i henhold til OpenAPI-spesifikasjonen, og man vil naturligvis søke etter biblioteker som integreres med OpenAPI. Men selv etter denne beslutningen oppstår det neste problemet:

Det finnes mange OpenAPI-relaterte biblioteker ... hvilke skal jeg bruke?

Dette dokumentet er en kort introduksjon til biblioteker, skrevet for Go-nybegynnere som opplever denne situasjonen. Dokumentet er skrevet per slutten av 2024, og siden språkmiljøet alltid er i endring, anbefales det å bruke dette som referanse og samtidig holde seg oppdatert på de nyeste utviklingene.

Bibliotekenes strategier for håndtering av OpenAPI

Som du kjenner til, er OpenAPI en spesifikasjon for å tydelig definere og dokumentere REST API-er. Ved å definere API-ets endepunkter, forespørsler og responsformater i YAML- eller JSON-format, hjelper det ikke bare utviklere, men også automatiserer kode generering for frontend og backend, reduserer meningsløs repetisjon og minimerer små menneskelige feil.

For å integrere OpenAPI sømløst i prosjekter, benytter bibliotekene i Go-økosystemet i hovedsak følgende tre strategier.

1. Kombinere Go-kommentarer til OpenAPI-spesifikasjonsdokumenter

En av utfordringene ved å utvikle API-er i henhold til OpenAPI er at det faktiske dokumentet og koden som implementerer det, eksisterer i separate filer på helt forskjellige steder. Dette fører ofte til situasjoner der koden oppdateres, men dokumentasjonen ikke, eller dokumentasjonen oppdateres, men koden ikke.

Et enkelt eksempel er:

  1. Du endrer logikken for et API i filen ./internal/server/user.go.
  2. Det faktiske dokumentet finnes i ./openapi3.yaml, og du kan glemme å oppdatere det.
  3. Hvis du sender en Pull Request uten å være klar over dette avviket og mottar anmeldelser fra kolleger,
  4. vil anmelderne heller ikke se endringene i ./openapi3.yaml. Dette kan føre til den uheldige situasjonen at API-spesifikasjonen forblir uendret, mens den faktiske API-implementeringen er endret.

Ved å skrive API-dokumentasjonen i form av Go-kommentarer kan man til en viss grad løse dette problemet. Siden koden og dokumentasjonen er samlet på ett sted, kan man oppdatere kommentarene samtidig som man endrer koden. Det finnes verktøy som automatisk genererer OpenAPI-spesifikasjonsdokumenter basert på slike kommentarer.

Et representativt prosjekt er Swag. Swag parser kommentarer i Go-kode og genererer OpenAPI 2-formatdokumenter. Bruken er enkel. Man skriver kommentarer over håndteringsfunksjonen i henhold til formatet som er definert av det aktuelle biblioteket.

 1// @Summary Opprett bruker
 2// @Description Oppretter en ny bruker.
 3// @Tags Users
 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 kommentarene er skrevet på denne måten, vil CLI-verktøyet Swag parse disse kommentarene og generere et OpenAPI 2-dokument. Denne prosessen utføres vanligvis som en del av CI-prosessen, og det genererte OpenAPI-spesifikasjonsdokumentet distribueres til Git Repository, det endelige byggeresultatet, eller et separat eksternt API-dokumenthåndteringssystem for samarbeid med andre prosjekter.

Fordeler:

  • Siden kommentarene er sammen med koden, reduseres sannsynligheten for at den faktiske koden og dokumentets form avviker.
  • Man kan enkelt og fritt dokumentere kun ved hjelp av kommentarer, uten behov for separate verktøy eller komplekse innstillinger.
  • Siden kommentarene ikke påvirker den faktiske API-logikken, er det bra for å legge til midlertidige funksjoner som er for belastende å publisere som dokumentasjon.

Ulemper:

  • Når antall kommentarlinjer øker, kan lesbarheten for en enkelt kodefil reduseres.
  • Det kan være vanskelig å uttrykke alle API-spesifikasjoner i form av kommentarer.
  • Siden dokumentasjonen ikke tvinger koden, er det ingen garanti for at OpenAPI-dokumentasjonen og den faktiske logikken stemmer overens.

2. Generere Go-kode fra OpenAPI-spesifikasjonsdokumenter

Det finnes også en metode hvor Single Source of Truth (SSOT) legges til dokumentasjonen i stedet for Go-koden. Dette innebærer å først definere OpenAPI-spesifikasjonen, og deretter generere Go-kode basert på det definerte innholdet. Siden API-spesifikasjonen genererer koden, kan man tvinge frem en utviklingskultur der API-design prioriteres. Utviklingsprosessen starter med definisjonen av API-spesifikasjonen, noe som har den fordelen at man tidlig kan forhindre uheldige situasjoner der oversettelser oppdages etter at utviklingen er fullført, noe som fører til API-spesifikasjonsendringer og en fullstendig revisjon av koden.

Representative prosjekter som benytter denne tilnærmingen inkluderer oapi-codegen og OpenAPI Generator. Bruken er enkel.

  1. Man skriver et yaml- eller json-dokument i henhold til OpenAPI-spesifikasjonen.
  2. Deretter kjører man CLI-en.
  3. Tilsvarende Go stub-kode genereres.
  4. Nå trenger man bare å implementere den detaljerte logikken for hvert enkelt API slik at denne stub-koden kan brukes.

Følgende er et eksempel på kode generert av 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}

Koden generert av oapi-codegen, med denne grensesnittet som medium, utfører logikk som parsing og validering av query parameters, header og body, og kaller den passende metoden deklarert i grensesnittet. Brukeren trenger kun å implementere implementasjonen av dette grensesnittet for å fullføre arbeidet som kreves for API-implementasjonen.

Fordeler:

  • Siden spesifikasjonen kommer først og utviklingen følger etter, er det en fordel for parallell utvikling når flere team samarbeider.
  • Koden for gjentatt og manuelt arbeid blir automatisk generert, noe som øker arbeidseffektiviteten samtidig som det fortsatt er fordelaktig for feilsøking.
  • Det er enklere å sikre at dokumentasjonen og kodens form alltid stemmer overens.

Ulemper:

  • Hvis man er uvitende om OpenAPI-spesifikasjonen, er det en viss innledende læringskurve.
  • Siden formen på koden som håndterer API-et genereres automatisk av prosjektet, kan det være vanskelig å tilpasse når tilpasning er nødvendig.

Forfatterens kommentar. Per oktober 2024 genererer OpenAPI Generator Go-kode som ikke bare tvinger API-logikken, men også hele prosjektstrukturen, noe som resulterer i en stiv prosjektstruktur. Dette gjør det uegnet for å legge til ulike funksjoner som er nødvendige i et produksjonsmiljø. De som velger denne metoden, anbefales sterkt å bruke oapi-codegen. Forfatteren bruker oapi-codegen + echo + StrictServerInterface.

3. Generere OpenAPI-spesifikasjonsdokumenter fra Go-kode

Når titalls eller hundrevis av mennesker utvikler på samme server, oppstår det uunngåelig at konsistensen brytes for individuelle API-er. Som et intuitivt eksempel, hvis spesifikasjonen for over 100 API-endepunkter deklareres i en enkelt OpenAPI yaml-fil, vil denne filen bli et monster på over 10 000 linjer. Ved deklarering av nye API-endepunkter vil det uunngåelig oppstå problemer som duplisering av samme modell, utelatelse av enkelte felt, eller inkonsekvent Path-navngivning som bryter den generelle API-konsistensen.

For å løse dette problemet kan man enten utpeke en egen eier for OpenAPI yaml-filen, eller utvikle en Linter for automatisk å fange opp slike avvik under CI-prosessen. Alternativt kan man definere et Domain-specific language (DSL) i Go-språket for å tvinge alle API-er til å ha en konsistent ensartethet.

Representative prosjekter som benytter denne teknikken er Kubernetes (bygger det selv uten et separat bibliotek), og man kan også bruke prosjekter som go-restful og goa. Følgende er et eksempel på bruk av 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 ovenfor, oppnår man fordelen av at implementeringen av POST /users API-et og definisjonen av dokumentasjonen fullføres samtidig.

Fordeler:

  • Siden alt kommer fra koden, er det enkelt å opprettholde API-konsistens for hele prosjektet.
  • Ved å utnytte Go's sterke typesystem kan man oppnå en mer nøyaktig og uomtvistelig spesifikasjon enn ved å bruke alle funksjonene i OpenAPI3.

Ulemper:

  • Man må lære DSL-en definert i hvert rammeverk, og det kan være vanskelig å anvende på eksisterende kode.
  • Siden man er tvunget til å følge reglene foreslått av rammeverket, kan friheten og fleksibiliteten reduseres.

Avslutningsvis

Hver metode har sine fordeler og ulemper, og det er viktig å velge den mest passende metoden basert på prosjektets krav og teamets preferanser. Det viktigste er ikke hvilken metode som er best, men å foreta en verdijudgment over hvilken løsning som passer best for den nåværende situasjonen, og å oppnå høy utviklingsproduktivitet for å kunne gå tidlig hjem og nyte en tilfredsstillende balanse mellom arbeid og fritid.

Selv om denne artikkelen er skrevet per oktober 2024, er Go- og OpenAPI-økosystemet i stadig utvikling. Derfor anbefales det å følge med på utviklingen av de ulike bibliotekene og prosjektene, samt eventuelle endringer i deres fordeler og ulemper, med tanke på tidsgapet fra når denne artikkelen leses.

Ha et lykkelig Go-liv! 😘