Generación de código
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
Actualizar a v3.31.1
La versión preliminar del generador ya es estable. Al actualizar desde v3.30.x, v3.31.1 incluye cambios incompatibles intencionados; consulte la guía de actualización antes de regenerar una aplicación existente. Fije el módulo y el comando de Goa a la misma versión, regenere todo el directoriogen/, y compile y pruebe la
aplicación. Coordine las actualizaciones de cliente y servidor para los formatos
de mensajes modificados que indica la guía. Para volver a la versión anterior,
restaure juntos las dependencias, el código generado y el código de la aplicación.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
- Crear diseño inicial
- Ejecutar
goa genpara generar el código base - Ejecuta
goa examplepara crear stubs de implementación - Implementa la lógica de tu servicio
- Ejecuta
goa gendespué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.
| Campo | Tipo de servicio | Cuerpo HTTP/JSON-RPC | Solicitud o respuesta protobuf |
|---|---|---|---|
| Primitivo requerido o con valor por defecto | Valor | Puntero al decodificar para validar; valor al codificar | Puntero para campos singulares cuya presencia debe conservarse |
| Primitivo opcional sin valor por defecto | Puntero | Puntero | Puntero |
| Objeto | Puntero | Puntero | Puntero |
| Array o mapa | Valor | Valor | Valor |
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
- Define los atributos de cada vista en el tipo de resultado.
- Goa genera las representaciones, conversiones y validadores de las vistas.
- 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:
- Añadir nuevos DSL - Construcciones de lenguaje de diseño adicionales
- 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