Go og OpenAPI-økosystemet

Indledning
Når man udvikler en Production Backend-server i Go, står næsten alle udviklere over for en af de første store udfordringer, nemlig følgende.
API-dokumentation, hvordan gør jeg det...?
Ved en kort søgning opdager man, at det er fordelagtigt at skrive dokumentation i overensstemmelse med OpenAPI-specifikationen, og man finder derefter naturligt frem til biblioteker, der integrerer med OpenAPI. Men selv når denne beslutning er truffet, opstår der et efterfølgende problem.
Der findes mange OpenAPI-relaterede biblioteker.. hvilket skal jeg bruge...?
Dette dokument er en kort introduktion til biblioteker, skrevet for Go-begyndere, der befinder sig i denne situation. Dokumentet er udarbejdet pr. udgangen af 2024, og da økosystemet konstant er under forandring, anbefales det at bruge det som reference og løbende følge med i de nyeste udviklinger.
Bibliotekernes strategier for OpenAPI
Det er sandsynligvis allerede velkendt, at OpenAPI er en specifikation til klart at definere og dokumentere REST API'er. Ved at definere API'ets endpoints, forespørgsler og svarformater i YAML- eller JSON-format bidrager den betydeligt til at automatisere kodegenerering for både udviklere, frontend og backend, hvilket reducerer unødvendig gentagelse og mindsker små menneskelige fejl.
For at integrere OpenAPI naturligt i et projekt anvender bibliotekerne i Go-økosystemet overordnet set følgende tre strategier.
1. Sammensætning af OpenAPI-specifikationsdokumentet ud fra Go-kommentarer
En vanskelighed ved at udvikle en API i overensstemmelse med OpenAPI er, at den faktiske dokumentation og den kode, der implementerer den, findes i separate filer på helt forskellige steder. Det er derfor ikke ualmindeligt, at koden opdateres uden at dokumentationen opdateres, eller at dokumentationen opdateres uden at koden opdateres.
Et enkelt eksempel:
- Logikken for en API ændres i filen
./internal/server/user.go - Den faktiske dokumentation findes i
./openapi3.yaml, og en ændring der er nødvendig dér kan let blive glemt. - Hvis man ikke opdager problemet, opretter en Pull Request og får gennemgang af kolleger,
- vil reviewerne heller ikke kunne se ændringerne i
./openapi3.yaml, hvilket kan medføre en uheldig situation, hvor API-specifikationen forbliver uændret, mens den faktiske API-implementering ændres.
Ved at skrive API-dokumentation som Go-kommentarer kan man i nogen grad løse dette problem. Eftersom kode og dokumentation samles ét sted, kan kommentarerne opdateres samtidig med, at koden ændres. Der findes værktøjer, der automatisk genererer OpenAPI-specifikationsdokumentet ud fra disse kommentarer.
Et repræsentativt projekt er Swag. Swag analyserer kommentarerne i Go-koden og genererer dokumentation i OpenAPI 2-format. Anvendelsen er enkel: Man skriver kommentarer i det format, som biblioteket har fastsat, over handler-funktionen.
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}
Når kommentarerne er skrevet på denne måde, analyserer CLI-værktøjet Swag dem og genererer et OpenAPI 2-dokument. Denne proces udføres typisk i CI-processen, og det genererede OpenAPI-specifikationsdokument distribueres til et Git-repository, til det endelige build-output eller til et separat system til administration af eksterne API-dokumentationer, så det kan anvendes ved samarbejde med andre projekter.
Fordele:
- Da kommentarerne findes sammen med koden, er risikoen for uoverensstemmelse mellem den faktiske kode og dokumentationen mindre.
- Uden særlige værktøjer eller komplekse konfigurationer kan man nemt og frit dokumentere med blot kommentarer.
- Da kommentarerne ikke påvirker den faktiske API-logik, er det godt til at tilføje midlertidige funktioner, som man er tilbageholdende med at offentliggøre i dokumentationen.
Ulemper:
- Når antallet af kommentarlinjer vokser, kan læsbarheden af en enkelt kodefil blive ringere.
- Det kan være vanskeligt at udtrykke alle API-specifikationer i form af kommentarer.
- Dokumentationen tvinger ikke koden til at følge den, så der kan ikke garanteres, at OpenAPI-dokumentet og den faktiske logik stemmer overens.
2. Generering af Go-kode ud fra OpenAPI-specifikationsdokumentet
Det er også muligt at placere Single Source of Truth (SSOT) i dokumentet frem for i Go-koden. Det vil sige, at OpenAPI-specifikationen defineres først, og Go-koden genereres ud fra den definerede indhold. Da API-specifikationen direkte genererer koden, kan man kulturelt set håndhæve, at API-designet udføres først, og eftersom specifikationsdefinitionen er det første trin i udviklingsforløbet, har metoden den styrke, at man tidligt kan opdage overset elementer og forhindre, at hele kodebasen skal ændres sammen med en ændring i API-specifikationen efter udviklingens afslutning.
To repræsentative projekter, der anvender denne metode, er oapi-codegen og OpenAPI Generator. Anvendelsen er enkel:
- Man skriver et OpenAPI-specifikationsdokument i YAML- eller JSON-format
- Man kører CLI'en
- Der genereres tilsvarende Go-stub-kode
- Herefter skal man blot implementere den detaljerede logik for hver enkelt API, så stub-koden kan anvendes.
Følgende er et eksempel på kode, der genereres af 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}
Den kode, som oapi-codegen genererer på baggrund af dette interface, udfører parsing og validering af query-parametre, headers og body og kalder den relevante metode, der er deklareret i interfacet. Brugeren behøver kun at implementere interfacet, hvorefter det nødvendige arbejde for API-implementeringen er afsluttet.
Fordele:
- Da specifikationen udarbejdes først, og udviklingen derefter følger, er det fordelagtigt for parallel udførelse af arbejdsopgaver, når flere teams samarbejder.
- Kode, der tidligere skulle skrives manuelt som gentagne rutineopgaver, genereres automatisk, hvilket øger arbejdseffektiviteten og samtidig giver fortsat god mulighed for fejlfinding.
- Det er let at sikre, at dokumentationen og koden altid stemmer overens.
Ulemper:
- Der er en vis indledende indlæringskurve, hvis man ikke kender OpenAPI-specifikationen godt.
- Da strukturen af koden, der håndterer API'et, genereres automatisk af projektet, kan det være vanskeligt at reagere, hvis der er behov for tilpasning.
Forfatterens kommentar. Pr. oktober 2024 genererer OpenAPI Generator Go-kode, der ikke blot håndhæver API-logikken, men hele projektets struktur, og denne struktur er stiv. Den er derfor uegnet til at tilføje de forskellige funktioner, der kræves i et egentligt Production-miljø. Til dem, der vælger denne metode, anbefales det kraftigt at bruge oapi-codegen. Forfatteren anvender oapi-codegen + echo + StrictServerInterface.
3. Generering af OpenAPI-specifikationsdokumentet ud fra Go-kode
Når titusindvis eller hundredvis af udviklere arbejder på den samme server, opstår nødvendigvis det problem, at ensartetheden mellem de enkelte API'er kan brydes. Et tydeligt eksempel er, at hvis specifikationerne for mere end 100 API-endpoints erklæres i én enkelt OpenAPI YAML-fil, bliver filen et monster på over 10.000 linjer. Når der tilføjes nye API-endpoints, vil den samme model nødvendigvis blive deklareret flere gange, nogle felter kan blive udeladt, eller der kan opstå stinavne, der ikke følger konventionerne, og dermed begynder den samlede ensartethed i API'et at briste.
For at løse dette problem kan man udpege en Owner til at administrere OpenAPI YAML-filen eller udvikle en Linter, der automatisk fanger fejlene under CI-processen. Men med Go kan man også definere et Domain-specific language (DSL), der tvinger alle API'er til at have en konsistent ensartethed.
Et repræsentativt projekt, der anvender denne teknik, er Kubernetes (som er bygget op uden eksterne biblioteker), og man kan også prøve at anvende projekter som go-restful og goa. Følgende er et eksempel på anvendelse 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})
Når man skriver kompilerbar Go-kode som ovenfor, får man den fordel, at implementeringen og dokumentationen for API'et POST /users defineres samtidigt.
Fordele:
- Da alt genereres ud fra koden, er det let at opretholde API-konsistens for hele projektet.
- Ved at udnytte Gos stærke typesystem opnår man en mere præcis og mindre omstridt specifikation end ved at anvende alle funktionerne i OpenAPI3.
Ulemper:
- Man skal lære det DSL, som hver framework har defineret, og det kan være vanskeligt at anvende på eksisterende kode.
- Da man skal følge de regler, som frameworket foreslår, kan frihed og fleksibilitet blive begrænset.
Afslutning
Hver metode har sine fordele og ulemper, og det er vigtigt at vælge den metode, der passer bedst til projektets krav og teamets præferencer. Det vigtigste er ikke, hvilken metode man bruger, men at vurdere, hvilken løsning der bedst passer til ens aktuelle situation, og at opnå høj udviklingsproduktivitet, så man kan nyde tidlig fyraften og en tilfredsstillende work-life balance.
Selvom denne artikel er skrevet pr. oktober 2024, udvikler økosystemerne for Go og OpenAPI sig konstant. Tag derfor hensyn til tiden mellem udgivelsen og læsningen, og følg løbende med i udviklingen af de enkelte biblioteker og projekter samt deres ændrede fordele og ulemper.
Hav en god Go-tid~ 😘