Go och OpenAPI-ekosystemet
Introduktion
När man utvecklar en produktions-Backend-server i Go är en av de första utmaningarna som nästan alla utvecklare stöter på följande:
Hur ska API-dokumentationen hanteras...?
Efter en kort undersökning inser man att det är fördelaktigt att skriva dokumentation som följer OpenAPI-specifikationen, och man söker naturligtvis efter bibliotek som integreras med OpenAPI. Men även efter detta beslut uppstår nästa problem.
Det finns många OpenAPI-relaterade bibliotek... vilket ska jag använda...?
Detta dokument är en kort introduktion till bibliotek, skriven för Go-nybörjare som upplever denna situation. Dokumentet är skrivet i slutet av 2024, och eftersom språkeekosystemet ständigt förändras, rekommenderas det att man använder detta som referens och alltid håller sig uppdaterad med de senaste utvecklingarna.
Strategier för bibliotek som hanterar OpenAPI
Som ni kanske redan vet är OpenAPI en specifikation för att tydligt definiera och dokumentera REST API:er. Genom att definiera API:ets slutpunkter, förfrågningar och svarsformat i YAML- eller JSON-format hjälper det inte bara utvecklare utan också att automatisera genereringen av frontend- och backend-kod, vilket minskar meningslös repetition och små mänskliga fel.
För att naturligt integrera OpenAPI i projektet antar Go-ekosystemets bibliotek i stort sett följande tre strategier.
1. Kombinera Go-kommentarer till OpenAPI-specifikationsdokument
En av de svåra aspekterna när man utvecklar API:er enligt OpenAPI är att den faktiska dokumentationen och den kod som implementerar den ofta finns i separata filer på helt olika platser. Detta leder till att man glömmer att uppdatera dokumentationen när koden uppdateras, eller att man uppdaterar dokumentationen men inte koden.
Ett enkelt exempel:
- Man ändrar logiken för ett API i filen
./internal/server/user.go, men - den faktiska dokumentationen finns i
./openapi3.yaml, och man kan av misstag glömma att göra motsvarande ändringar där. - Om man skickar in en Pull Request utan att ha uppmärksammat dessa ändringar och får den granskad av kollegor,
- kommer granskarna inte heller att se ändringarna i
./openapi3.yaml. Detta kan leda till den olyckliga situationen att API-specifikationen förblir oförändrad, medan den faktiska API-implementeringen har ändrats.
Genom att skriva API-dokumentationen i form av Go-kommentarer kan man delvis lösa detta problem. Eftersom koden och dokumentationen finns samlade på ett ställe kan man uppdatera kommentarerna samtidigt som man ändrar koden. Det finns verktyg som automatiskt genererar OpenAPI-specifikationsdokument baserat på dessa kommentarer.
Ett framstående projekt är Swag. Swag analyserar kommentarer i Go-kod och genererar OpenAPI 2-formatdokumentation. Användningen är enkel. Man skriver kommentarer ovanför handlarfunktionerna enligt det format som definieras av respektive bibliotek.
1// @Summary Skapa användare
2// @Description Skapar en ny användare.
3// @Tags Användare
4// @Accept json
5// @Produce json
6// @Param user body models.User true "Användarinformation"
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 dessa kommentarer har skrivits, analyserar CLI-verktyget Swag dem för att generera ett OpenAPI 2-dokument. Denna process utförs vanligtvis som en del av CI-processen, och det genererade OpenAPI-specifikationsdokumentet distribueras sedan till Git Repository, det slutliga byggresultatet, eller ett externt API-dokumenthanteringssystem för samarbete med andra projekt.
Fördelar:
- Eftersom kommentarerna finns tillsammans med koden minskar risken att den faktiska koden och dokumentationen skiljer sig åt.
- Dokumentationen kan göras enkelt och fritt enbart med kommentarer, utan behov av separata verktyg eller komplexa konfigurationer.
- Eftersom kommentarerna inte påverkar den faktiska API-logiken, är det lämpligt att lägga till tillfälliga funktioner som är för känsliga för att offentliggöras i dokumentationen.
Nackdelar:
- Antalet kommentarer kan öka, vilket kan försämra läsbarheten för en enskild kodfil.
- Det kan vara svårt att uttrycka alla API-specifikationer i kommentarform.
- Eftersom dokumentationen inte tvingar koden, kan det inte garanteras att OpenAPI-dokumentationen och den faktiska logiken överensstämmer.
2. Generera Go-kod från OpenAPI-specifikationsdokument
Det finns även en metod där "Single Source of Truth" (SSOT) placeras i dokumentationen istället för i Go-koden. Detta innebär att man först definierar OpenAPI-specifikationen och sedan genererar Go-kod baserat på den definierade informationen. Eftersom API-specifikationen direkt genererar koden, kan detta tvinga fram en utvecklingskultur där API-design prioriteras. Utvecklingsprocessen börjar med att definiera API-specifikationen, vilket har fördelen att det tidigt kan förhindra olyckliga situationer där man inser missade delar först efter att utvecklingen är klar, vilket leder till ändringar i API-specifikationen och en fullständig omstrukturering av koden.
Framstående projekt som använder denna metod inkluderar oapi-codegen och OpenAPI Generator. Användningen är enkel.
- Skriv ett yaml- eller json-dokument enligt OpenAPI-specifikationen.
- Kör CLI-verktyget.
- Motsvarande Go stub-kod genereras.
- Nu behöver du bara implementera den detaljerade logiken för varje enskilt API så att denna stub kan användas.
Följande är ett exempel på kod som genereras 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 som genereras av oapi-codegen med detta interface som grund hanterar logik för att tolka och validera query parameters, headers och body, samt anropar den lämpliga metoden som deklarerats i interfacet. Användaren behöver bara implementera en konkret implementation av detta interface för att slutföra arbetet med API-implementeringen.
Fördelar:
- Eftersom specifikationen kommer först och utvecklingen följer, är det fördelaktigt att arbeta parallellt när flera team samarbetar.
- Kod för repetitiva, mödosamma uppgifter genereras automatiskt, vilket ökar effektiviteten samtidigt som det fortfarande är fördelaktigt för felsökning.
- Det är lätt att garantera att dokumentationen och koden alltid stämmer överens.
Nackdelar:
- Om man är okunnig om OpenAPI-specifikationen i sig, finns det en viss initial inlärningskurva.
- Eftersom koden som hanterar API:et genereras automatiskt av projektet, kan det vara svårt att anpassa den om anpassning krävs.
Författarens kommentar. Per oktober 2024 genererar OpenAPI Generator Go-kod som inte bara tvingar API-logiken utan också hela projektstrukturen, vilket resulterar i en styv projektstruktur. Den genererade koden är olämplig för att lägga till de olika funktioner som krävs i en produktionsmiljö. För dem som väljer denna metod rekommenderas starkt att använda oapi-codegen. Författaren använder oapi-codegen + echo + StrictServerInterface.
3. Generera OpenAPI-specifikationsdokument från Go-kod
När tiotals eller hundratals människor utvecklar för samma server uppstår oundvikligen problemet att enskilda API:er kan förlora sin enhetlighet. Ett intuitivt exempel är om man deklarerar specifikationen för över 100 API-slutpunkter i en enda OpenAPI YAML-fil, kommer den filen att bli ett monster på över 10 000 rader. När nya API-slutpunkter deklareras, kommer man oundvikligen att duplicera modeller, utelämna vissa fält eller skapa Path-namn som inte följer konventionen, vilket leder till att den övergripande enhetligheten i API:et bryts.
För att lösa detta problem kan man antingen utse en separat ägare för OpenAPI YAML-filen, eller utveckla en Linter som automatiskt fångar upp sådana problem under CI-processen. Men man kan också definiera ett Domain-specific language (DSL) i Go för att tvinga alla API:er att ha en konsekvent enhetlighet.
Kubernetes är ett framstående projekt som använder denna teknik (byggt internt utan separata bibliotek), och man kan också använda projekt som go-restful och goa. Följande är ett exempel på användning 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})
Genom att skriva kompilerbar Go-kod som ovan, får man fördelen att implementeringen av POST /users API:et och definitionen av dokumentationen slutförs samtidigt.
Fördelar:
- Eftersom allt härstammar från koden, är det enkelt att upprätthålla en konsekvent API-standard för hela projektet.
- Genom att utnyttja Go:s starka typsystem kan man uppnå en mer exakt och otvetydig specifikation än när man använder alla funktioner i OpenAPI3.
Nackdelar:
- Man måste lära sig det DSL som definieras av varje ramverk, och det kan vara svårt att tillämpa det på befintlig kod.
- Eftersom man tvingas följa de regler som föreslås av ramverket, kan flexibiliteten och anpassningsförmågan minska.
Sammanfattningsvis
Varje metod har sina för- och nackdelar, och det är viktigt att välja den lämpligaste metoden baserat på projektets krav och teamets preferenser. Det viktigaste är inte vilken metod som är bäst, utan att göra en värdering av vilken lösning som är mest lämplig för den aktuella situationen, öka utvecklingsproduktiviteten för att kunna gå hem tidigt och njuta av en tillfredsställande balans mellan arbete och fritid.
Även om denna text skrevs i oktober 2024, utvecklas Go- och OpenAPI-ekosystemen ständigt. Därför rekommenderas det att man kontinuerligt följer upp de senaste utvecklingarna för varje bibliotek och projekt, samt deras förändrade för- och nackdelar, med hänsyn till tidsgapet sedan denna text skrevs.
Ha ett lyckligt Go-liv! 😘