Go og OpenAPI-økosystemet
Introduktion
Når man udvikler en Production Backend-server i Go, er en af de første udfordringer, næsten alle udviklere støder på, følgende:
API-dokumentation, hvordan gør man det...?
Ved at søge lidt om dette emne finder man ud af, at det er fordelagtigt at skrive dokumentation, der overholder OpenAPI-specifikationen, og man begynder naturligt at lede efter biblioteker, der kan integreres med OpenAPI. Men selv efter at have truffet denne beslutning, opstår det næste problem:
Der er mange OpenAPI-relaterede biblioteker... hvilken skal jeg bruge...?
Dette dokument er en kort introduktion til biblioteker, skrevet til Go-begyndere, der oplever denne situation. Dokumentet er skrevet med udgangspunkt i slutningen af 2024, og da sprogøkosystemet altid er dynamisk og i forandring, anbefales det at bruge dette som reference, men også altid at holde øje med de seneste opdateringer.
Bibliotekernes strategier for at håndtere OpenAPI
Som du måske allerede ved, er OpenAPI en specifikation for klart at definere og dokumentere REST API'er. Ved at definere API'ens endpoints, anmodninger og svarformater i YAML- eller JSON-format hjælper det ikke kun udviklere, men automatiserer også kode-generering på frontend og backend, hvilket reducerer meningsløs gentagelse og minimerer små menneskelige fejl.
For at integrere OpenAPI naturligt i projekter anvender Go-økosystemets biblioteker primært følgende tre strategier:
1. Kombinering af Go-kommentarer til OpenAPI-specifikationsdokumenter
En af de vanskelige aspekter ved at udvikle API'er i overensstemmelse med OpenAPI er, at den faktiske dokumentation og den kode, der implementerer den, ofte findes i separate filer på helt forskellige steder. Dette fører til, at man hyppigt glemmer at opdatere dokumentationen, når koden opdateres, eller omvendt.
Et simpelt eksempel er:
- Man ændrer logikken for en API i filen
./internal/server/user.go. - Den faktiske dokumentation findes i
./openapi3.yaml, og man kan ved et uheld glemme at opdatere denne. - Hvis man indsender en Pull Request uden at være opmærksom på dette ændringsissue og får feedback fra kolleger,
- vil anmelderne heller ikke se ændringerne i
./openapi3.yaml. Dette kan føre til den uheldige situation, at API-specifikationen forbliver den samme, mens den faktiske API-implementering er ændret.
Ved at skrive API-dokumentation i form af Go-kommentarer kan man til en vis grad løse dette problem. Da kode og dokumentation er samlet ét sted, kan man opdatere kommentarerne, når koden ændres. Der findes værktøjer, der automatisk genererer OpenAPI-specifikationsdokumenter baseret på disse kommentarer.
Et fremtrædende projekt er Swag. Swag parser Go-kodekommentarer og genererer dokumenter i OpenAPI 2-format. Brugen er enkel: Man skriver kommentarer over handler-funktionerne i det format, der er defineret af hvert bibliotek.
1// @Summary Opret bruger
2// @Description Opretter en ny bruger.
3// @Tags Brugere
4// @Accept json
5// @Produce json
6// @Param user body models.User sand "Brugerinformation"
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 disse kommentarer er skrevet, parser et CLI-værktøj som Swag dem og genererer et OpenAPI 2-dokument. Denne proces udføres typisk som en del af CI-pipelinen, og det genererede OpenAPI-specifikationsdokument distribueres derefter til et Git Repository, det endelige build-resultat eller et eksternt API-dokumentationssystem for at blive brugt i samarbejde med andre projekter.
Fordele:
- Da kommentarerne er sammen med koden, reduceres sandsynligheden for uoverensstemmelser mellem den faktiske kode og dokumentationen.
- Enkel og fleksibel dokumentation kan opnås udelukkende med kommentarer, uden behov for separate værktøjer eller kompleks opsætning.
- Da kommentarer ikke påvirker den faktiske API-logik, er det ideelt til at tilføje midlertidige funktioner, der kan være følsomme at offentliggøre i dokumentationen.
Ulemper:
- Når antallet af kommentarer stiger, kan læsbarheden af den enkelte kodefil forringes.
- Det kan være vanskeligt at udtrykke alle API-specifikationer i form af kommentarer.
- Da dokumentationen ikke håndhæver koden, kan man ikke garantere, at OpenAPI-dokumentationen og den faktiske logik stemmer overens.
2. Generering af Go-kode ud fra OpenAPI-specifikationsdokumenter
Der findes også en metode, hvor "Single Source of Truth" (SSOT) placeres på dokumentsiden i stedet for Go-koden. Dette involverer først at definere OpenAPI-specifikationen og derefter generere Go-kode baseret på det definerede indhold. Da API-specifikationen direkte genererer koden, kan det kulturelt tvinge API-design til at være den første prioritet i udviklingsprocessen. Da API-specifikationen er det første skridt i udviklingen, har denne tilgang den fordel, at den tidligt kan forhindre uheldige situationer, hvor mangler først opdages efter udviklingen er afsluttet, hvilket fører til ændringer i API-specifikationen og en komplet omskrivning af koden.
Fremtrædende projekter, der anvender denne tilgang, inkluderer oapi-codegen og OpenAPI Generator. Brugsmetoden er enkel:
- Man skriver et YAML- eller JSON-dokument i overensstemmelse med OpenAPI-specifikationen.
- Derefter kører man CLI'en.
- Dette genererer den tilsvarende Go-stub-kode.
- Nu skal man blot implementere den specifikke logik for de individuelle API'er, så stubben kan anvendes.
Følgende er et eksempel på kode genereret af oapi-codegen:
1// StrictServerInterface repræsenterer alle server-handlers.
2type StrictServerInterface interface {
3 // ...
4 // Returnerer alle kæledyr
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
Koden genereret af oapi-codegen med denne interface som grundlag udfører logik som parsing og validering af query parameters, headers og body, og kalder derefter den passende metode, der er deklareret i interfacet. Brugeren skal kun implementere en implementering for denne interface, og arbejdet med at implementere API'en er derefter afsluttet.
Fordele:
- Da specifikationen kommer først, og udviklingen følger, er det fordelagtigt for parallel udvikling, når flere teams samarbejder.
- Kode til gentagne manuelle opgaver genereres automatisk, hvilket øger arbejdseffektiviteten og stadig er fordelagtigt for debugging.
- Det er lettere at sikre, at dokumentation og kode altid stemmer overens.
Ulemper:
- Hvis man er uvidende om OpenAPI-specifikationen, er der en indledende indlæringskurve.
- Da koden, der håndterer API'en, genereres automatisk af projektet, kan det være vanskeligt at tilpasse, hvis tilpasning er nødvendig.
Forfatterens kommentar. Pr. oktober 2024 genererer OpenAPI Generator Go-kode, der ikke kun omfatter API-logik, men også tvinger hele projektstrukturen, hvilket gør projektstrukturen stiv og uegnet til at tilføje forskellige funktioner, der er nødvendige i et faktisk produktionsmiljø. For dem, der vælger denne metode, anbefaler jeg kraftigt at bruge oapi-codegen. Forfatteren bruger oapi-codege + echo + StrictServerInterface.
3. Generering af OpenAPI-specifikationsdokumenter fra Go-kode
Når titusinder eller hundreder af mennesker udvikler på den samme server, opstår der uundgåeligt det problem, at konsistensen kan blive brudt mellem individuelle API'er. Som et intuitivt eksempel, hvis specifikationerne for over 100 API-endpoints deklareres i en enkelt OpenAPI YAML-fil, vil denne fil blive et monster på over 10.000 linjer, og når nye API-endpoints deklareres, vil der uundgåeligt opstå situationer, hvor den samme model deklareres dobbelt, nogle felter udelades, eller Path-navngivning, der ikke stemmer overens med konventionerne, opstår, hvilket begynder at bryde den overordnede API-konsistens.
For at løse dette problem kan man udpege en separat ejer til at administrere OpenAPI YAML, eller man kan udvikle en Linter for automatisk at fange uoverensstemmelser under CI-processen. Men man kan også definere et Domain-specific language (DSL) i Go for at tvinge alle API'er til at have en ensartet konsistens.
Fremtrædende projekter, der anvender denne teknik, er Kubernetes (som er selvudviklet uden et separat bibliotek), og man kan også bruge projekter som go-restful og goa. Følgende er et eksempel på brug af 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 at skrive kompilerbar Go-kode som vist ovenfor opnår man den fordel, at implementeringen og dokumentationsdefinitionen for POST /users API'en udføres samtidigt.
Fordele:
- Da alt udspringer af koden, er det nemt at opretholde API-konsistens for hele projektet.
- Ved at udnytte Go's stærke typesystem kan man opnå mere præcise og utvetydige specifikationer end ved at udnytte alle funktioner i OpenAPI3.
Ulemper:
- Man skal lære DSL'en defineret af hvert framework, og det kan være vanskeligt at anvende den på eksisterende kode.
- Da man er tvunget til at følge de regler, der foreslås af frameworket, kan friheden og fleksibiliteten være begrænset.
Afslutning
Hver metode har sine fordele og ulemper, og det er vigtigt at vælge den mest passende metode afhængigt af projektets krav og teamets præferencer. Det vigtigste er ikke, hvilken metode der er bedst, men at foretage en værdidom og vælge den løsning, der passer bedst til den aktuelle situation, for at opnå høj udviklingsproduktivitet og nyde tidlig fyraften og et tilfredsstillende work-life balance.
Selvom denne artikel er skrevet med udgangspunkt i oktober 2024, udvikler Go- og OpenAPI-økosystemet sig konstant. Derfor anbefales det at følge med i de seneste opdateringer og ændrede fordele og ulemper ved de forskellige biblioteker og projekter, idet der tages højde for tidsforskellen mellem skrivetidspunktet og læsetidspunktet.
God Go-liv! 😘