Go och OpenAPI-ekosystemet

Inledning
När man utvecklar en produktionsbackend-server i Go stöter nästan alla utvecklare tidigt på en av de första svårigheterna, nämligen följande.
Hur dokumenterar man API:et...?
Med en kort efterforskning inser man att det är fördelaktigt att skriva dokumentation enligt OpenAPI-specifikationen, och man söker sedan naturligt efter bibliotek som integreras med OpenAPI. Men även när detta beslut är fattat uppstår nästa problem.
Det finns så många OpenAPI-relaterade bibliotek... vilket ska man använda...?
Detta dokument är en kortfattad introduktion till bibliotek, skriven för nybörjare i Go som befinner sig i denna situation. Dokumentet är skrivet per slutet av 2024, och eftersom språkekosystemet ständigt förändras rekommenderas att man använder det som referens samtidigt som man följer med i de senaste utvecklingarna.
Bibliotekens strategier för OpenAPI
Som många redan känner till är OpenAPI en specifikation för att tydligt definiera och dokumentera REST API:er. Genom att definiera API:ets endpoints, requests och responseformat i YAML- eller JSON-format bidrar den starkt till att automatisera kodgenerering för både frontend och backend, vilket minskar onödig upprepning och reducerar mindre mänskliga fel, inte bara för utvecklare.
För att naturligt integrera OpenAPI i ett projekt tar bibliotek i Go-ekosystemet i huvudsak följande tre strategier.
1. Sammanställning av OpenAPI-specifikationsdokument från Go-kommentarer
En svårighet när man utvecklar API:er enligt OpenAPI är att den faktiska dokumentationen och koden som implementerar den finns i separata filer på helt olika platser, vilket gör att det är ganska vanligt att koden uppdateras men dokumentationen inte, eller att dokumentationen uppdateras men inte koden.
Ett enkelt exempel:
- Man ändrar API-logiken i filen
./internal/server/user.go - Den faktiska dokumentationen finns i
./openapi3.yaml, och man kan av misstag glömma att ändra den. - Om man skapar en Pull Request utan att märka problemet och kollegor granskar den
- kan även granskarna, eftersom ändringarna i
./openapi3.yamlinte syns, släppa igenom en situation där API-specifikationen förblir oförändrad medan den faktiska API-implementationen har ändrats.
Genom att skriva API-dokumentation som Go-kommentarer kan dessa problem till viss del lösas. Eftersom kod och dokumentation finns på samma ställe kan man uppdatera kommentarerna samtidigt som man ändrar koden. Det finns verktyg som automatiskt genererar OpenAPI-specifikationsdokument baserat på dessa kommentarer.
Ett representativt projekt är Swag. Swag tolkar kommentarerna i Go-koden och genererar dokumentation i OpenAPI 2-format. Användningen är enkel: man skriver kommentarer enligt det format som varje bibliotek definierar ovanför 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 kommentarerna är skrivna tolkar CLI-verktyget Swag dessa kommentarer och genererar ett OpenAPI 2-dokument. Vanligtvis körs detta i CI-processen, och det genererade OpenAPI-specifikationsdokumentet publiceras i Git-repositoryt, i slutlig byggartefakt eller i ett separat externt system för API-dokumentation, så att det kan användas vid samarbete med andra projekt.
Fördelar:
- Eftersom kommentarerna finns tillsammans med koden minskar risken för att den faktiska koden och dokumentationen avviker från varandra.
- Man kan dokumentera enkelt och flexibelt med endast kommentarer, utan särskilda verktyg eller komplex konfiguration.
- Eftersom kommentarerna inte påverkar den faktiska API-logiken är det lätt att lägga till tillfälliga funktioner som man inte vill exponera i dokumentationen.
Nackdelar:
- När antalet kommentarrader ökar kan läsbarheten för en enskild kodfil försämras.
- Det kan vara svårt att uttrycka hela API-specifikationen enbart med kommentarer.
- Dokumentationen tvingar inte fram överensstämmelse med koden, så det går inte att garantera att OpenAPI-dokumentet och den faktiska logiken stämmer överens.
2. Generering av Go-kod från OpenAPI-specifikationsdokument
Det finns också ett sätt där Single Source of Truth (SSOT) placeras i dokumentet snarare än i Go-koden. Det innebär att man först definierar OpenAPI-specifikationen och sedan genererar Go-kod utifrån den definierade informationen. Eftersom API-specifikationen i sig genererar koden kan man införa en utvecklingskultur där API-design görs först, och eftersom definitionen av API-specifikationen är det första steget i utvecklingsprocessen har metoden styrkan att tidigt förhindra att man först efter att utvecklingen är klar upptäcker förbisedda delar och därefter måste ändra API-specifikationen och hela kodbasen.
Representativa projekt som använder detta tillvägagångssätt är oapi-codegen och OpenAPI Generator. Användningen är enkel.
- Skriv ett YAML- eller JSON-dokument enligt OpenAPI-specifikationen
- Kör CLI:t
- Motsvarande Go-stubkod genereras.
- Nu behöver man bara implementera den detaljerade logiken för respektive enskilt API så att stubben 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}
Den kod som oapi-codegen genererar utifrån detta interface utför parsning av query parameters, headers och body samt validering, och anropar sedan lämplig metod som deklarerats i interfacet. Användaren behöver endast implementera detta interface för att API-implementationen ska vara klar.
Fördelar:
- Eftersom specifikationen tas fram först och utvecklingen sedan följer är det fördelaktigt för parallellt arbete när flera team samarbetar.
- Koden för de delar som tidigare krävde repetitivt manuellt arbete genereras automatiskt, vilket ökar arbetseffektiviteten och fortfarande är fördelaktigt för felsökning.
- Det är lätt att säkerställa att dokumentationen och koden alltid överensstämmer.
Nackdelar:
- För den som saknar kunskap om OpenAPI-specifikationen finns en viss initial inlärningströskel.
- Eftersom koden som hanterar API:et genereras automatiskt av projektet kan det vara svårt att hantera situationer där anpassning krävs.
Författarens kommentar. Per oktober 2024 genererar OpenAPI Generator Go-kod som påtvingar inte bara API-logiken utan hela projektstrukturen, och eftersom strukturen är stel är koden mindre lämpad för att lägga till de många funktioner som krävs i en produktionsmiljö. För dem som väljer detta tillvägagångssätt rekommenderas starkt att använda oapi-codegen. Författaren använder oapi-codegen + echo + StrictServerInterface.
3. Generering av OpenAPI-specifikationsdokument från Go-kod
När tiotals eller hundratals personer utvecklar samma server uppstår oundvikligen problemet att enhetligheten mellan olika API:er kan brytas. Ett tydligt exempel är att om specifikationen för över 100 API-endpoints deklareras i en enda OpenAPI YAML-fil blir filen ett monster på över 10 000 rader, och när nya API-endpoints läggs till uppstår oundvikligen dubbeldeklarerade modeller, saknade fält eller path-namn som inte följer konventionen, vilket gradvis bryter den övergripande enhetligheten i API:et.
För att lösa detta kan man tillsätta en separat ägare för OpenAPI YAML-filen eller utveckla en linter som automatiskt upptäcker problem under CI-processen. Men med Go kan man också definiera ett Domain-specific language (DSL) och på så sätt tvinga fram en konsekvent enhetlighet för alla API:er.
Ett representativt projekt som använder denna teknik är Kubernetes (som byggs upp helt självständigt utan separata bibliotek). Man kan också prova 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 på detta sätt får man den fördelen att implementationen av API:et POST /users och dess dokumentationsdefinition blir klara samtidigt.
Fördelar:
- Eftersom allt genereras från koden är det lätt att upprätthålla API-enhetlighet över hela projektet.
- Genom att använda Gos starka typsystem kan man få en mer exakt och mindre kontroversiell specifikation än när man utnyttjar alla funktioner i OpenAPI3.
Nackdelar:
- Man måste lära sig den DSL som varje ramverk definierar, och det kan vara svårt att tillämpa på befintlig kod.
- Eftersom man måste följa de regler som ramverket föreslår kan frihet och flexibilitet minska.
Avslutning
Varje metod har sina för- och nackdelar, och det är viktigt att välja den metod som passar projektets krav och teamets preferenser. Det viktigaste är inte vilken metod som är bäst, utan att bedöma vilken lösning som bäst passar den situation man befinner sig i och att hålla hög utvecklingsproduktivitet, så att man kan njuta av en tidig hemgång och en tillfredsställande balans mellan arbete och privatliv.
Även om texten är skriven utifrån oktober 2024 utvecklas ekosystemet för Go och OpenAPI kontinuerligt. Ta därför hänsyn till tiden mellan när du läser detta och när texten skrevs, och fortsätt att följa med i de aktuella utvecklingarna samt de förändrade för- och nackdelarna hos varje bibliotek och projekt.
Ha en trevlig Go-tid~ 😘