Go e l'ecosistema OpenAPI

Introduzione
Quando si sviluppa un server backend di produzione con Go, quasi tutti gli sviluppatori si imbattono per primi in una delle sfide più ardue, ovvero la seguente.
Come documentare le API...?
Cercando un po' si scopre che è vantaggioso scrivere documentazione conforme alla specifica OpenAPI, e naturalmente si finisce per cercare una libreria che si integri con OpenAPI. Tuttavia, anche dopo aver preso questa decisione, si presenta il problema successivo.
Esistono molte librerie legate a OpenAPI... quale dovrei usare...?
Questo documento è una breve introduzione alle librerie, scritta per i principianti di Go che si trovano in questa situazione. È stato redatto alla fine del 2024; poiché l'ecosistema linguistico cambia continuamente, si consiglia di consultarlo tenendo sempre d'occhio le ultime novità.
Le strategie delle librerie nei confronti di OpenAPI
Probabilmente lo sapete già, ma OpenAPI è una specifica per definire e documentare in modo chiaro le REST API. Definendo in formato YAML o JSON gli endpoint, le richieste e i formati di risposta delle API, essa non solo aiuta gli sviluppatori, ma contribuisce in modo significativo ad automatizzare la generazione di codice lato frontend e lato backend, riducendo le ripetizioni inutili e i piccoli errori umani.
Per integrare in modo naturale OpenAPI nei progetti, le librerie dell'ecosistema Go adottano principalmente tre strategie.
1. Comporre la specifica OpenAPI a partire dai commenti Go
Una difficoltà nello sviluppo di API conformi a OpenAPI è che il documento reale e il codice che lo implementa risiedono in file separati, in posizioni completamente diverse. Di conseguenza, capita piuttosto spesso di aggiornare il codice senza aggiornare il documento, oppure di aggiornare il documento senza riuscire ad aggiornare il codice.
Ecco un semplice esempio.
- Si modifica la logica di un'API nel file
./internal/server/user.go - Il documento reale si trova in
./openapi3.yaml, e si può dimenticare per errore di apportarvi la modifica. - Senza accorgersi del problema, si invia una Pull Request e la si sottopone alla revisione dei colleghi.
- Anche i revisori, non vedendo le modifiche a
./openapi3.yaml, potrebbero trovarsi nella spiacevole situazione in cui la specifica dell'API resta invariata mentre l'implementazione effettiva dell'API cambia.
Scrivere la documentazione delle API sotto forma di commenti Go permette di risolvere in parte questo problema. Poiché codice e documentazione si trovano nello stesso posto, è possibile aggiornare i commenti contestualmente alla modifica del codice. Esistono strumenti che generano automaticamente la specifica OpenAPI a partire da questi commenti.
Un progetto rappresentativo è Swag. Swag analizza i commenti del codice Go e genera documentazione in formato OpenAPI 2. L'utilizzo è semplice: basta scrivere i commenti, nel formato definito da ciascuna libreria, sopra la funzione handler.
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}
Una volta scritti questi commenti, il CLI di Swag li analizza e genera il documento OpenAPI 2. Generalmente questa operazione viene eseguita nel processo di CI, e la specifica OpenAPI generata viene pubblicata nel Git Repository, nel risultato finale della build o in un sistema esterno di gestione della documentazione delle API, per essere utilizzata nella collaborazione con altri progetti.
Vantaggi:
- Poiché i commenti si trovano insieme al codice, è meno probabile che codice e documentazione divergano.
- Senza strumenti aggiuntivi o configurazioni complesse, è possibile documentare in modo semplice e flessibile usando solo i commenti.
- Poiché i commenti non influenzano la logica reale dell'API, è comodo aggiungere funzionalità temporanee che non si vuole rendere pubbliche nella documentazione.
Svantaggi:
- All'aumentare del numero di righe di commento, la leggibilità di un singolo file di codice può diminuire.
- Potrebbe risultare difficile esprimere tutte le specifiche delle API solo tramite commenti.
- La documentazione non impone il codice, quindi non è garantito che la specifica OpenAPI corrisponda alla logica reale.
2. Generare il codice Go a partire dal documento OpenAPI
Esiste anche un approccio che colloca la Single Source of Truth (SSOT) non nel codice Go ma nel documento. Si definisce prima la specifica OpenAPI e si genera il codice Go in base a quanto definito. Poiché la specifica dell'API genera direttamente il codice, è possibile imporre culturalmente la progettazione delle API prima dell'implementazione. Inoltre, definendo per primo la specifica nel processo di sviluppo, si evita in anticipo lo spiacevole scenario in cui, a sviluppo concluso, ci si accorge di qualcosa di trascurato e si deve modificare l'intero codice insieme alla specifica.
I progetti rappresentativi che adottano questo approccio sono oapi-codegen e OpenAPI Generator. L'utilizzo è semplice.
- Si scrive un documento YAML o JSON conforme alla specifica OpenAPI
- Si esegue il CLI
- Viene generato il corrispondente stub di codice Go
- Non resta che implementare la logica dettagliata di ciascuna API, in modo che possa essere utilizzata dallo stub.
Di seguito un esempio del codice generato da 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}
Il codice generato da oapi-codegen, basato su questa interface, esegue il parsing dei query parameters, degli header e del body, nonché la Validation, e richiama il metodo appropriato dichiarato nell'interface. L'utente deve soltanto implementare la struct che realizza l'interface, e il lavoro necessario per l'implementazione dell'API è completato.
Vantaggi:
- Poiché la specifica viene definita per prima e lo sviluppo procede successivamente, è vantaggioso per la collaborazione tra più team, che possono lavorare in parallelo.
- Il codice relativo alle parti ripetitive e manuali viene generato automaticamente, quindi si ottiene un aumento dell'efficienza mantenendo un buon supporto al debugging.
- È facile garantire che documento e codice restino sempre coerenti.
Svantaggi:
- Chi non conosce la specifica OpenAPI può incontrare una curva di apprendimento iniziale non trascurabile.
- Poiché la struttura del codice che gestisce le API viene generata automaticamente in base al progetto, potrebbe risultare difficile reagire quando è necessaria una personalizzazione.
Commento dell'autore. Al ottobre 2024, il codice Go generato da OpenAPI Generator impone non solo la logica delle API ma l'intera struttura del progetto, e genera codice con una struttura rigida, poco adatta all'aggiunta delle molteplici funzionalità richieste in un ambiente di produzione reale. Per chi adotta questo approccio, si consiglia vivamente di utilizzare oapi-codegen. L'autore utilizza oapi-codegen + echo + StrictServerInterface.
3. Generare la specifica OpenAPI a partire dal codice Go
Quando decine o centinaia di persone sviluppano per lo stesso server, un problema che emerge inevitabilmente è la perdita di uniformità tra le singole API. Ad esempio, se le specifiche di oltre 100 endpoint API vengono dichiarate in un unico file YAML OpenAPI, tale file diventa un mostro di oltre 10.000 righe; nel dichiarare nuovi endpoint, inevitabilmente si dichiarano più volte gli stessi modelli, si omettono alcuni campi o nascono nomi di path che non rispettano le convenzioni, e così l'uniformità complessiva dell'API inizia a incrinarsi.
Per risolvere questi problemi si potrebbe affidare la gestione del file YAML OpenAPI a un Owner dedicato, oppure sviluppare un Linter che individui automaticamente tali problemi durante il processo di CI. Tuttavia, definendo un Domain-specific language (DSL) con Go, è possibile imporre che tutte le API seguano una coerenza uniforme.
Un progetto rappresentativo che utilizza questa tecnica è Kubernetes (che la realizza internamente, senza librerie esterne), mentre si possono provare progetti come go-restful e goa. Di seguito un esempio d'uso di 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})
Scrivendo codice Go compilabile come quello sopra, si ottiene il vantaggio che l'implementazione dell'API POST /users e la definizione della sua documentazione vengono completate contemporaneamente.
Vantaggi:
- Poiché tutto deriva dal codice, è facile mantenere la coerenza delle API in tutto il progetto.
- Sfruttando il sistema di tipi forti di Go, si ottiene una specifica più precisa e priva di ambiguità rispetto a quella che si otterrebbe usando tutte le funzionalità di OpenAPI3.
Svantaggi:
- È necessario apprendere il DSL definito da ciascun framework, e può essere difficile applicarlo a codice esistente.
- Poiché è necessario seguire obbligatoriamente le regole proposte dal framework, la libertà e la flessibilità possono risultare ridotte.
Considerazioni conclusive
Ogni metodo presenta vantaggi e svantaggi, ed è importante scegliere quello più adatto in base ai requisiti del progetto e alle preferenze del team. La cosa più importante non è quale metodo sia migliore in assoluto, ma valutare quale soluzione sia più adatta alla propria situazione concreta e mantenere un'elevata produttività, così da godere di uscite anticipate dal lavoro e di un equilibrio tra vita e lavoro soddisfacente.
Sebbene questo articolo sia stato scritto con riferimento a ottobre 2024, l'ecosistema di Go e OpenAPI è in continua evoluzione; si consiglia quindi di seguire costantemente le novità delle singole librerie e dei progetti, tenendo conto del tempo trascorso dal momento in cui si legge questo testo, e di aggiornarsi sui loro cambiamenti in termini di vantaggi e svantaggi.
Buon viaggio nel mondo di Go~ 😘