Generazione del codice
La generazione di codice di Goa trasforma il progetto in contratti di servizio,
trasporti, client e documentazione pronti per la produzione. goa example crea
il collegamento iniziale eseguibile, mentre l’applicazione fornisce la logica di
business.
Strumenti a riga di comando
Installazione
GOPROXY=direct go install goa.design/goa/v3/cmd/goa@fix/goa-generation-plan
Provare una versione preliminare della generazione
Le versioni preliminari sono facoltative. Fissare il modulo Goa e il comandogoa allo stesso commit. Il lavoro corrente si trova nel
branch preliminare fix/goa-generation-plan,
al commit
318c40614944e151ec7de2cfb712e0d08b73f7af.
Rigenerare l’intera directory gen/, senza mai mescolare output stabile e
preliminare, quindi compilare e provare l’intera applicazione. Coordinare gli
aggiornamenti di client e server quando la guida segnala una modifica al formato
scambiato. Per tornare alla versione stabile, fissare insieme il modulo e il
comando stabili e rigenerare di nuovo tutto.Comandi
Tutti i comandi si aspettano i percorsi di importazione dei pacchetti Go, non i percorsi del filesystem:
# ✅ Correct: using Go package import path
goa gen goa.design/examples/calc/design
# ❌ Incorrect: using filesystem path
goa gen ./design
Generare codice (goa gen)
goa gen <design-package-import-path> [-o <output-dir>]
È il comando principale per la generazione del codice:
- Elabora il pacchetto di progettazione e genera il codice di implementazione
- Ricrea ogni volta da zero l’intera directory
gen/ - Viene eseguito dopo ogni modifica del progetto
Crea un esempio (goa example)
goa example <design-package-import-path> [-o <output-dir>]
Un comando di impalcatura:
- Crea un’implementazione di esempio una tantum
- Genera stub di gestori con la logica dell’esempio
- Viene eseguito una volta all’avvio di un nuovo progetto
- NON sovrascrive l’implementazione personalizzata esistente
Mostra la versione
goa version
Flusso di lavoro dello sviluppo
- Creare il progetto iniziale
- Eseguire
goa genper generare il codice base - Eseguire
goa exampleper creare stub di implementazione - Implementare la logica del servizio
- Eseguire
goa gendopo ogni modifica alla progettazione
Best Practice: Impegnare il codice generato nel controllo di versione, anziché generarlo durante il CI/CD. Questo assicura build riproducibili e permette di tracciare le modifiche nel codice generato.
Processo di generazione
Quando si esegue goa gen, Goa segue un processo sistematico:
1. Fase di bootstrap
Goa crea un main.go temporaneo che:
- Importa i pacchetti Goa e il pacchetto di progettazione
- Esegue la valutazione del DSL
- Genera il codice
2. Valutazione del progetto
- Le funzioni DSL vengono eseguite per creare oggetti espressione
- Le espressioni si combinano in un modello API completo
- Si stabiliscono le relazioni tra le espressioni
- Convalida delle regole e dei vincoli di progettazione
3. Generazione del codice
- Le espressioni convalidate passano ai generatori di codice
- I modelli eseguono il rendering per produrre i file di codice
- L’output viene scritto nella cartella
gen/
Prima del rendering, Goa determina pacchetti, dichiarazioni, nomi, importazioni, percorsi dei campi e rami noti a partire dal progetto completo e convalidato. I modelli scrivono direttamente queste scelte. I programmi generati scelgono un ramo solo in base ai valori ricevuti durante l’esecuzione.
Struttura del codice generato
Un tipico progetto generato:
myservice/
├── cmd/ # Generated example commands
│ └── calc/
│ ├── grpc.go
│ └── http.go
├── design/ # Your design files
│ └── design.go
├── gen/ # Generated code (don't edit)
│ ├── calc/ # Service-specific code
│ │ ├── client.go
│ │ ├── endpoints.go
│ │ └── service.go
│ ├── http/ # HTTP transport layer
│ │ ├── calc/
│ │ │ ├── client/
│ │ │ └── server/
│ │ └── openapi.json
│ └── grpc/ # gRPC transport layer
│ └── calc/
│ ├── client/
│ ├── server/
│ └── pb/
└── myservice.go # Your service implementation
Interfacce di servizio
Generato in gen/<service>/service.go:
// Service interface defines the API contract
type Service interface {
Add(context.Context, *AddPayload) (res int, err error)
Multiply(context.Context, *MultiplyPayload) (res int, err error)
}
// Payload types
type AddPayload struct {
A int32
B int32
}
// Constants for observability
const ServiceName = "calc"
var MethodNames = [2]string{"add", "multiply"}
Livello del punto finale
Generato in gen/<service>/endpoints.go:
// Endpoints wraps service methods in transport-agnostic endpoints
type Endpoints struct {
Add goa.Endpoint
Multiply goa.Endpoint
}
// NewEndpoints creates endpoints from service implementation
func NewEndpoints(s Service) *Endpoints {
return &Endpoints{
Add: NewAddEndpoint(s),
Multiply: NewMultiplyEndpoint(s),
}
}
// Use applies middleware to all endpoints
func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) {
e.Add = m(e.Add)
e.Multiply = m(e.Multiply)
}
Esempio di middleware endpoint:
func LoggingMiddleware(next goa.Endpoint) goa.Endpoint {
return func(ctx context.Context, req any) (res any, err error) {
log.Printf("request: %v", req)
res, err = next(ctx, req)
log.Printf("response: %v", res)
return
}
}
endpoints.Use(LoggingMiddleware)
Codice client
Generato in gen/<service>/client.go:
// Client provides typed methods for service calls
type Client struct {
AddEndpoint goa.Endpoint
MultiplyEndpoint goa.Endpoint
}
func NewClient(add, multiply goa.Endpoint) *Client {
return &Client{
AddEndpoint: add,
MultiplyEndpoint: multiply,
}
}
func (c *Client) Add(ctx context.Context, p *AddPayload) (res int, err error) {
ires, err := c.AddEndpoint(ctx, p)
if err != nil {
return
}
return ires.(int), nil
}
Generazione del codice HTTP
Implementazione del server
Generato in gen/http/<service>/server/server.go:
func New(
e *calc.Endpoints,
mux goahttp.Muxer,
decoder func(*http.Request) goahttp.Decoder,
encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
errhandler func(context.Context, http.ResponseWriter, error),
formatter func(ctx context.Context, err error) goahttp.Statuser,
) *Server
// Server exposes handlers for modification
type Server struct {
Mounts []*MountPoint
Add http.Handler
Multiply http.Handler
}
// Use applies HTTP middleware to all handlers
func (s *Server) Use(m func(http.Handler) http.Handler)
Configurazione completa del server:
func main() {
svc := calc.New()
endpoints := gencalc.NewEndpoints(svc)
mux := goahttp.NewMuxer()
server := genhttp.New(
endpoints,
mux,
goahttp.RequestDecoder,
goahttp.ResponseEncoder,
nil, nil)
genhttp.Mount(mux, server)
http.ListenAndServe(":8080", mux)
}
Implementazione del client
Generato in gen/http/<service>/client/client.go:
func NewClient(
scheme string,
host string,
doer goahttp.Doer,
enc func(*http.Request) goahttp.Encoder,
dec func(*http.Response) goahttp.Decoder,
restoreBody bool,
) *Client
Configurazione completa del client:
func main() {
httpClient := genclient.NewClient(
"http",
"localhost:8080",
http.DefaultClient,
goahttp.RequestEncoder,
goahttp.ResponseDecoder,
false,
)
client := gencalc.NewClient(
httpClient.Add(),
httpClient.Multiply(),
)
result, err := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
}
Generazione del codice gRPC
Definizione di protobuf
Generato in gen/grpc/<service>/pb/:
syntax = "proto3";
package calc;
service Calc {
rpc Add (AddRequest) returns (AddResponse);
rpc Multiply (MultiplyRequest) returns (MultiplyResponse);
}
message AddRequest {
int64 a = 1;
int64 b = 2;
}
Implementazione del server
func main() {
svc := calc.New()
endpoints := gencalc.NewEndpoints(svc)
svr := grpc.NewServer()
gensvr := gengrpc.New(endpoints, nil)
genpb.RegisterCalcServer(svr, gensvr)
lis, _ := net.Listen("tcp", ":8080")
svr.Serve(lis)
}
Implementazione del client
func main() {
conn, _ := grpc.Dial("localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
grpcClient := genclient.NewClient(conn)
client := gencalc.NewClient(
grpcClient.Add(),
grpcClient.Multiply(),
)
result, _ := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
}
Personalizzazione
Controllo della generazione dei tipi
Forza la generazione di tipi non direttamente referenziati dai metodi:
var MyType = Type("MyType", func() {
// Force generation in specific services
Meta("type:generate:force", "service1", "service2")
// Or force generation in all services
Meta("type:generate:force")
Attribute("name", String)
})
Organizzazione del pacchetto
Generare tipi in un pacchetto condiviso:
var CommonType = Type("CommonType", func() {
Meta("struct:pkg:path", "types")
Meta("type:generate:force")
Attribute("id", String)
})
Crea:
gen/
└── types/
└── common_type.go
struct:pkg:path assegna al tipo definito nel progetto una sola dichiarazione
nel pacchetto generato selezionato, e ogni utilizzo generato importa tale
dichiarazione. Il nome del pacchetto Go è l’ultimo segmento del percorso in
minuscolo. Se il tipo spostato contiene un altro tipo definito nel progetto,
anche quella dipendenza deve dichiarare esplicitamente struct:pkg:path, in
genere con lo stesso pacchetto. I tipi annidati creati dal compilatore rimangono
accanto al tipo definito nel progetto a cui appartengono.
Una dichiarazione definita nel progetto viene riutilizzata tra servizi e tra gli usi come payload, risultato ed errore. Quando quel tipo esatto è un errore personalizzato, Goa aggiunge i metodi di errore accanto alla stessa dichiarazione invece di generare un secondo tipo.
Personalizzazione del campo
var Message = Type("Message", func() {
Attribute("id", String, func() {
// Override field name
Meta("struct:field:name", "ID")
// Add custom struct tags
Meta("struct:tag:json", "id,omitempty")
Meta("struct:tag:msgpack", "id,omitempty")
// Override type
Meta("struct:field:type", "bson.ObjectId", "github.com/globalsign/mgo/bson", "bson")
})
})
Personalizzazione del buffer di protocollo
var MyType = Type("MyType", func() {
// Override protobuf message name
Meta("struct:name:proto", "CustomProtoType")
Field(1, "status", Int32, func() {
// Override protobuf field type
Meta("struct:field:proto", "int32")
})
// Use Google's timestamp type
Field(2, "created_at", String, func() {
Meta("struct:field:proto",
"google.protobuf.Timestamp",
"google/protobuf/timestamp.proto",
"Timestamp",
"google.golang.org/protobuf/types/known/timestamppb")
})
})
// Specify protoc include paths
var _ = API("calc", func() {
Meta("protoc:include", "/usr/include", "/usr/local/include")
})
Personalizzazione di OpenAPI
Per impostazione predefinita Goa genera documenti OpenAPI 2.0 e 3.0. Per generare anche una descrizione OpenAPI 3.2.0, selezionala esplicitamente a livello di API:
var _ = API("MyAPI", func() {
Meta("openapi:versions", "2.0", "3.0", "3.2")
Meta("openapi:path:3.2", "docs/openapi")
})
Le versioni selezionate vengono scritte sia in JSON sia in YAML. L’esempio
precedente genera gen/docs/openapi.json e gen/docs/openapi.yaml per
OpenAPI 3.2; senza la sostituzione del percorso, Goa scrive
gen/http/openapi3.2.json e gen/http/openapi3.2.yaml. La selezione delle
versioni non modifica il codice di servizio generato.
var _ = API("MyAPI", func() {
// Control generation
Meta("openapi:generate", "false")
// Format JSON output
Meta("openapi:json:prefix", " ")
Meta("openapi:json:indent", " ")
// Disable example generation
Meta("openapi:example", "false")
})
var _ = Service("UserService", func() {
// Add tags
HTTP(func() {
Meta("openapi:tag:Users")
Meta("openapi:tag:Backend:desc", "Backend API Operations")
})
Method("CreateUser", func() {
// Custom operation ID
Meta("openapi:operationId", "{service}.{method}")
// Custom summary
Meta("openapi:summary", "Create a new user")
HTTP(func() {
// Add extensions
Meta("openapi:extension:x-rate-limit", `{"rate": 100}`)
POST("/users")
})
})
})
var User = Type("User", func() {
// Override type name in OpenAPI spec
Meta("openapi:typename", "CustomUser")
})
Tipi e convalida
Applicazione della convalida
Goa convalida i dati ai confini del sistema:
- Lato server: Convalida le richieste in entrata
- Lato client: Convalida le risposte in arrivo
- Codice interno: Fiducioso per mantenere gli invarianti
Regole sui puntatori per i campi delle strutture
I tipi di servizio rappresentano valori già convalidati. I tipi di trasporto decodificati devono anche conservare l’assenza di un campo in ingresso.
| Campo | Tipo di servizio | Body HTTP/JSON-RPC | Richiesta o risposta protobuf |
|---|---|---|---|
| Primitivo richiesto o con valore predefinito | Valore | Puntatore durante la decodifica per convalidare la presenza; valore durante la codifica | Puntatore per i campi singoli la cui presenza deve essere conservata |
| Primitivo facoltativo senza valore predefinito | Puntatore | Puntatore | Puntatore |
| Oggetto | Puntatore | Puntatore | Puntatore |
| Array o mappa | Valore | Valore | Valore |
Per HTTP e JSON-RPC, l’input decodificato è una richiesta sul server o una risposta sul client. Le richieste codificate dal client e le risposte codificate dal server usano valori. Negli struct protobuf, booleani, numeri, stringhe, enum e relativi alias singoli richiesti sono puntatori sia nelle richieste sia nelle risposte. La convalida distingue così un campo omesso da un valore zero esplicito. Le slice di byte restano slice, i messaggi restano puntatori e gli struct di servizio mantengono la propria struttura.
Esempio:
type Person struct {
Name string // required, direct value
Age *int // optional, pointer
Hobbies []string // array, no pointer
Metadata map[string]string // map, no pointer
}
ArrayOfRequired usa puntatori per elementi primitivi e alias primitivi solo
nei body HTTP e JSON-RPC in ingresso, per rifiutare [null]. Il servizio e le
risposte generate usano slice di valori.
Gestione dei valori predefiniti
- Marshaling: I valori predefiniti inizializzano array/mappe nulli
- Unmarshaling: I valori predefiniti si applicano ai campi opzionali mancanti (non ai campi obbligatori mancanti)
Viste e tipi di risultato
Le viste controllano il modo in cui i tipi di risultato sono resi nelle risposte.
Come funzionano le viste
- Il metodo del servizio include un parametro di vista
- Un pacchetto di viste viene generato a livello di servizio
- La validazione specifica della vista viene generata automaticamente
Risposta lato server
- Il tipo di risultato visualizzato è marshallizzato
- Gli attributi Nil sono omessi
- Il nome della vista viene passato nell’intestazione “Goa-View”
Risposta lato client
- La risposta è non marshallata
- Trasformata nel tipo di risultato visualizzato
- Nome della vista estratto dall’intestazione “Goa-View”
- Esecuzione della validazione specifica della vista
- Riconvertito in tipo di risultato del servizio
Vista predefinita
Se non sono state definite viste, Goa aggiunge una vista “predefinita” che include tutti i campi di base.
Sistema di plugin
Il sistema di plugin di Goa estende la generazione del codice. I plugin possono:
- Aggiungere nuovi DSL - Costrutti aggiuntivi del linguaggio di progettazione
- Modificare il codice generato - Ispezionare e modificare i file, aggiungere nuovi file
Esempio di utilizzo del plugin CORS:
import (
. "goa.design/goa/v3/dsl"
cors "goa.design/plugins/v3/cors/dsl"
)
var _ = Service("calc", func() {
cors.Origin("/.*localhost.*/", func() {
cors.Headers("X-Shared-Secret")
cors.Methods("GET", "POST")
})
})
Casi d’uso comuni dei plugin:
- Supporto del protocollo (CORS, ecc.)
- Formati di documentazione aggiuntivi
- Regole di validazione personalizzate
- Aspetti trasversali (registrazione, metriche)
- Generazione di file di configurazione
Le callback pubblicate restano adatte ai plugin che modificano valori o file generati. Un plugin che dichiara un nome a livello di pacchetto deve usare la fase di pianificazione della factory, così Goa può riservare quel nome insieme a tutte le altre dichiarazioni prima del rendering. Consultare l’architettura della generazione di codice e la guida all’aggiornamento della versione preliminare per il contratto dettagliato dei plugin e i passaggi di migrazione.
Vedi anche
- Riferimento DSL - Riferimento DSL completo per i file di progetto
- Guida HTTP - Funzionalità e personalizzazione del trasporto HTTP
- Guida gRPC - Caratteristiche del trasporto gRPC e buffer di protocollo
- Quickstart - Per iniziare con la generazione di codice