Generación de código

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

La generación de código de Goa transforma su diseño en contratos de servicio, transportes, clientes y documentación listos para producción. goa example crea el cableado inicial ejecutable, mientras que su aplicación aporta la lógica de negocio.

Herramientas de línea de comandos

Instalación

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

Comandos

Todos los comandos esperan rutas de importación de paquetes Go, no rutas de sistemas de ficheros:

# ✅ Correct: using Go package import path
goa gen goa.design/examples/calc/design

# ❌ Incorrect: using filesystem path
goa gen ./design

Generar código (goa gen)

goa gen <design-package-import-path> [-o <output-dir>]

El comando principal para la generación de código:

  • Procesa su paquete de diseño y genera código de implementación
  • Recrea todo el directorio gen/ desde cero cada vez
  • Se ejecuta después de cada cambio de diseño

Crear ejemplo (goa example)

goa example <design-package-import-path> [-o <output-dir>]

Un comando de andamiaje:

  • Crea una implementación de ejemplo de una sola vez
  • Genera stubs de manejadores con lógica de ejemplo
  • Se ejecuta una vez al iniciar un nuevo proyecto
  • NO sobrescribirá la implementación personalizada existente

Mostrar versión

goa version

Flujo de trabajo de desarrollo

  1. Crear diseño inicial
  2. Ejecutar goa gen para generar el código base
  3. Ejecuta goa example para crear stubs de implementación
  4. Implementa la lógica de tu servicio
  5. Ejecuta goa gen después de cada cambio de diseño

Mejor práctica: Confirmar el código generado al control de versiones en lugar de generarlo durante CI/CD. Esto asegura construcciones reproducibles y permite el seguimiento de los cambios en el código generado.


Proceso de generación

Cuando se ejecuta goa gen, Goa sigue un proceso sistemático:

1. Fase de arranque

Goa crea un main.go temporal que:

  • Importa los paquetes de Goa y tu paquete de diseño
  • Ejecuta la evaluación DSL
  • Activa la generación de código

2. Evaluación del diseño

  • Las funciones DSL se ejecutan para crear objetos de expresión
  • Las expresiones se combinan en un modelo API completo
  • Se establecen relaciones entre las expresiones
  • Se validan las reglas de diseño y las restricciones

3. Generación de código

  • Las expresiones validadas pasan a los generadores de código
  • Las plantillas se renderizan para producir archivos de código
  • La salida se escribe en el directorio gen/

Goa resuelve los paquetes, las declaraciones, los nombres, las importaciones, las rutas de los campos y las ramas conocidas a partir del diseño completo y validado antes de renderizar. Las plantillas escriben directamente esas decisiones. Los programas generados solo se ramifican según los valores que reciben durante la ejecución.


Estructura del código generado

Un proyecto generado típico:

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

Interfaces de servicio

Generadas en 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"}

Capa de endpoints

Generada en 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)
}

Ejemplo de middleware de punto final:

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)

Código cliente

Generado en 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
}

Generación de código HTTP

Implementación del servidor

Generado en 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)

Configuración completa del servidor:

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)
}

Implementación del cliente

Generado en 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

Configuración completa del cliente:

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})
}

Generación de código gRPC

Definición Protobuf

Generado en 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;
}

Implementación del servidor

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)
}

Implementación Cliente

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})
}

Personalización

Control de generación de tipos

Forzar la generación de tipos no referenciados directamente por métodos:

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)
})

Organización del paquete

Generar tipos en un paquete compartido:

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 da al tipo definido por el autor una única declaración en el paquete generado seleccionado, y cada uso generado importa esa declaración. El nombre del paquete Go es el último segmento de la ruta en minúsculas. Si el tipo reubicado contiene otro tipo definido por el autor, esa dependencia también debe declarar un struct:pkg:path explícito, normalmente el mismo paquete. Los tipos anidados creados por el compilador permanecen junto al tipo definido por el autor al que pertenecen.

Una declaración definida por el autor se reutiliza entre servicios y entre usos como carga útil, resultado y error. Cuando ese tipo exacto es un error personalizado, Goa añade los métodos de error junto a la misma declaración en vez de generar un segundo tipo.

Personalización de campos

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")
    })
})

Personalización del búfer de protocolo

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")
})

Personalización de OpenAPI

Por defecto, Goa genera documentos OpenAPI 2.0 y 3.0. Para generar también una descripción OpenAPI 3.2.0, selecciónela explícitamente en el nivel de la API:

var _ = API("MyAPI", func() {
    Meta("openapi:versions", "2.0", "3.0", "3.2")
    Meta("openapi:path:3.2", "docs/openapi")
})

Las versiones seleccionadas se escriben tanto en JSON como en YAML. El ejemplo anterior genera gen/docs/openapi.json y gen/docs/openapi.yaml para OpenAPI 3.2; sin la ruta personalizada, Goa escribe gen/http/openapi3.2.json y gen/http/openapi3.2.yaml. La selección de versiones no cambia el código de servicio generado.

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")
})

Tipos y validación

Aplicación de la validación

Los decodificadores de transporte generados por Goa validan las peticiones entrantes en el servidor y las respuestas entrantes en el cliente. El servicio mantiene después las invariantes de la aplicación. Las llamadas directas a un servicio o endpoint generado omiten esa decodificación; sus llamadores deben proporcionar valores válidos o validar en su propio punto de entrada.

Reglas de puntero para campos Struct

Los tipos de servicio representan valores ya validados. Los tipos de transporte decodificados también deben conservar si un campo entrante estaba ausente.

Esta tabla describe campos escalares y objetos habituales en Goa v3.31.1. Bytes, Any y las uniones tienen representaciones propias; consulta sus tipos generados.

CampoTipo de servicioCuerpo HTTP/JSON-RPCSolicitud o respuesta protobuf
Primitivo requerido o con valor por defectoValorPuntero al decodificar para validar; valor al codificarPuntero para campos singulares cuya presencia debe conservarse
Primitivo opcional sin valor por defectoPunteroPunteroPuntero
ObjetoPunteroPunteroPuntero
Array o mapaValorValorValor

En HTTP y JSON-RPC, la entrada decodificada es una solicitud en el servidor o una respuesta en el cliente. Los campos escalares obligatorios o con valor por defecto usan valores en las solicitudes codificadas por el cliente y las respuestas codificadas por el servidor; los escalares opcionales sin valor por defecto siguen siendo punteros. En los structs protobuf, los booleanos, números, strings, enums y sus alias singulares requeridos son punteros tanto en solicitudes como en respuestas. Así la validación distingue un campo omitido de un valor cero explícito. Los slices de bytes siguen siendo slices, los mensajes siguen siendo punteros y los structs de servicio conservan su estructura.

Ejemplo:

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 punteros para elementos primitivos y alias primitivos solo en cuerpos HTTP y JSON-RPC entrantes, para rechazar [null]. El servicio y las respuestas generadas usan slices de valores.

Presencia de colecciones

Required("items") y MinLength(1) expresan restricciones distintas. En JSON, una colección obligatoria debe estar presente y no ser null, pero [] o {} es válido salvo que una restricción de longitud lo impida. Los campos repeated y map de protobuf no distinguen ausencia y vacío tras la serialización y deserialización; la validación generada comprueba su longitud y contenido, no su presencia. Los escalares singulares, mensajes y oneofs obligatorios conservan sus propias comprobaciones de presencia.

No uses colecciones Go nil frente a vacías para representar operaciones del dominio. Modela una operación explícita cuando sea necesario distinguir «dejar sin cambios» de «reemplazar por vacío».

Manejo de valores por defecto

Los valores por defecto pertenecen al diseño y los aplican las conversiones de transporte generadas. Al decodificar gRPC en la versión preliminar, un valor ausente recibe el valor por defecto declarado; un 0, false o valor vacío explícito se conserva. La conversión del servicio a protobuf conserva el valor del servicio y no sustituye ceros por valores por defecto.

HTTP usa reglas distintas: los constructores de cuerpos entrantes aplican valores por defecto a los valores ausentes, y los de cuerpos salientes pueden aplicarlos a campos de servicio con valor cero. Consulta el constructor y decodificador generados para la dirección pertinente cuando cero o ausencia tenga significado; no apliques una sola regla a todos los transportes.


Vistas y tipos de resultados

Las vistas controlan cómo se muestran los tipos de resultados en las respuestas.

Cómo funcionan las vistas

  1. Define los atributos de cada vista en el tipo de resultado.
  2. Goa genera las representaciones, conversiones y validadores de las vistas.
  3. Un método puede seleccionar una vista fija en el diseño. Para un resultado unario dinámico, el método generado devuelve el nombre de vista junto al resultado; las interfaces de streaming exponen la operación generada para seleccionar la vista.

Respuesta del lado del servidor

El codificador generado selecciona la representación de la vista elegida. Excluye los atributos ajenos a esa vista; los atributos obligatorios incluidos siguen formando parte del contrato. Las vistas HTTP dinámicas llevan el nombre en la cabecera Goa-View, y gRPC usa los metadatos goa-view. En la versión preliminar, JSON-RPC representa una vista dinámica con { "view": ..., "body": ... } dentro de result; los resultados sin vista o con vista fija no usan ese envoltorio.

Respuesta del lado del cliente

El cliente generado lee la vista seleccionada, decodifica su representación, valida su contrato y la convierte al resultado del servicio. Los clientes personalizados deben seguir la representación del transporte y la versión seleccionados.

Vista por defecto

Si no se definen vistas, Goa añade una vista “por defecto” que incluye todos los campos básicos.


Sistema de plugins

El sistema de plugins de Goa extiende la generación de código. Los plugins pueden:

  1. Añadir nuevos DSL - Construcciones de lenguaje de diseño adicionales
  2. Modificar el código generado - Inspeccionar y modificar archivos, añadir nuevos archivos

Ejemplo de uso 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")
    })
})

Casos de uso comunes del plugin:

  • Soporte de protocolos (CORS, etc.)
  • Formatos de documentación adicionales
  • Reglas de validación personalizadas
  • Cuestiones transversales (registro, métricas)
  • Generación de archivos de configuración

Las funciones de callback publicadas siguen siendo adecuadas para los plugins que editan valores o archivos generados. Un plugin que declara un nombre a nivel de paquete debe usar la fase de planificación de la factoría para que Goa pueda reservar ese nombre junto con todas las demás declaraciones antes de renderizar. Consulte la arquitectura de generación de código y la guía de actualización para conocer el contrato detallado de los plugins y los pasos de migración.


Véase también

  • Referencia DSL - Referencia DSL completa para archivos de diseño
  • HTTP Guide - Características y personalización del transporte HTTP
  • Guía gRPC - Características del transporte gRPC y búferes de protocolo
  • Quickstart - Introducción a la generación de código