Generazione del codice

Markdown
Complete guide to Goa’s code generation - commands, process, generated code structure, and customization options.

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

go get goa.design/goa/v3@v3.31.1
go install goa.design/goa/v3/cmd/goa@v3.31.1

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

  1. Creare il progetto iniziale
  2. Eseguire goa gen per generare il codice base
  3. Eseguire goa example per creare stub di implementazione
  4. Implementare la logica del servizio
  5. Eseguire goa gen dopo 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

I decoder di trasporto generati da Goa convalidano le richieste in ingresso sul server e le risposte in ingresso sul client. Il servizio mantiene poi gli invarianti dell’applicazione. Le chiamate dirette a un servizio o endpoint generato non passano da questa decodifica; i chiamanti devono fornire valori validi oppure convalidarli al proprio punto di ingresso.

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.

Questa tabella descrive i comuni campi scalari e oggetto in Goa v3.31.1. Bytes, Any e le unioni hanno rappresentazioni proprie; consulta i tipi generati.

CampoTipo di servizioBody HTTP/JSON-RPCRichiesta o risposta protobuf
Primitivo richiesto o con valore predefinitoValorePuntatore durante la decodifica per convalidare la presenza; valore durante la codificaPuntatore per i campi singoli la cui presenza deve essere conservata
Primitivo facoltativo senza valore predefinitoPuntatorePuntatorePuntatore
OggettoPuntatorePuntatorePuntatore
Array o mappaValoreValoreValore

Per HTTP e JSON-RPC, l’input decodificato è una richiesta sul server o una risposta sul client. I campi scalari obbligatori o con valore predefinito usano valori nelle richieste codificate dal client e nelle risposte codificate dal server; gli scalari facoltativi senza valore predefinito restano puntatori. 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.

Presenza delle collezioni

Required("items") e MinLength(1) esprimono vincoli diversi. In JSON, una collezione obbligatoria deve essere presente e non null, ma [] o {} è valido se un vincolo di lunghezza non lo impedisce. I campi repeated e map di protobuf non distinguono assenza e vuoto dopo serializzazione e deserializzazione; la convalida generata controlla lunghezza e contenuto, non la presenza. Scalari singoli, messaggi e oneof obbligatori conservano i propri controlli di presenza.

Non usare collezioni Go nil rispetto a vuote per rappresentare operazioni di dominio. Modella un’operazione esplicita quando occorre distinguere «lascia invariato» da «sostituisci con una collezione vuota».

Gestione dei valori predefiniti

I valori predefiniti appartengono al design e vengono applicati dalle conversioni di trasporto generate. Nella decodifica gRPC della versione preliminare, un valore assente riceve il valore predefinito dichiarato; uno 0, false o valore vuoto esplicito viene conservato. La conversione dal servizio a protobuf conserva il valore fornito e non sostituisce gli zeri con valori predefiniti.

HTTP segue regole diverse: i costruttori dei body in ingresso applicano i valori predefiniti ai valori assenti, e quelli in uscita possono applicarli ai campi del servizio con valore zero. Consulta il costruttore e il decoder generati nella direzione pertinente quando zero o assenza hanno significato di dominio; non applicare un’unica regola a tutti i trasporti.


Viste e tipi di risultato

Le viste controllano il modo in cui i tipi di risultato sono resi nelle risposte.

Come funzionano le viste

  1. Definisci gli attributi di ogni vista nel tipo di risultato.
  2. Goa genera rappresentazioni, conversioni e validatori delle viste.
  3. Un metodo può selezionare una vista fissa nel design. Per un risultato unario dinamico, il metodo generato restituisce il nome della vista insieme al risultato; le interfacce di streaming espongono l’operazione generata per selezionare la vista.

Risposta lato server

Il codificatore generato seleziona la rappresentazione della vista scelta. Gli attributi esterni alla vista sono esclusi; gli attributi obbligatori inclusi restano parte del contratto. Le viste HTTP dinamiche trasmettono il nome nell’header Goa-View, mentre gRPC usa i metadati goa-view. Nella versione preliminare, JSON-RPC rappresenta una vista dinamica con { "view": ..., "body": ... } dentro result; i risultati senza vista o con vista fissa non usano questa struttura.

Risposta lato client

Il client generato legge la vista selezionata, decodifica la rappresentazione, ne convalida il contratto e la converte nel risultato del servizio. I client personalizzati devono seguire la rappresentazione del trasporto e della versione selezionati.

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:

  1. Aggiungere nuovi DSL - Costrutti aggiuntivi del linguaggio di progettazione
  2. 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 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