Guía gRPC
Goa proporciona un soporte completo para la construcción de servicios gRPC a través de su DSL y la generación de código. Esta guía cubre el diseño de servicios, patrones de streaming, gestión de errores e implementación.
Visión general
El soporte gRPC de Goa incluye:
- Generación automática de búferes de protocolo: archivos
.protogenerados a partir de su diseño - Seguridad de tipos: Seguridad de tipos de extremo a extremo desde la definición hasta la implementación
- Generación de código: Código de servidor y cliente generado automáticamente
- Validación incorporada: Solicitud de validación basada en su diseño
- Soporte de streaming: Soporte de todos los patrones gRPC
- Gestión de errores: Gestión integral de errores con asignación de códigos de estado
Asignación de tipos
| Tipo de Goa | Tipo de Buffer de Protocolo |
|---|---|
| Int | int32 |
| Int32 | int32 |
| Int64 Int64 | |
| UInt Uint32 | |
| UInt32 Uint32 | |
| UInt64 Uint64 | |
| Float32 Float | |
| Float64 Double | |
| String String | |
| Boolean Bool | |
| Bytes Bytes | |
| ArrayOf repetido | |
| MapOf Map |
Diseño del servicio
Estructura básica del servicio
var _ = Service("calculator", func() {
Description("The Calculator service performs arithmetic operations")
GRPC(func() {
Metadata("package", "calculator.v1")
Metadata("go.package", "calculatorpb")
})
Method("add", func() {
Description("Add two numbers")
Payload(func() {
Field(1, "a", Int, "First operand")
Field(2, "b", Int, "Second operand")
Required("a", "b")
})
Result(func() {
Field(1, "sum", Int, "Result of addition")
Required("sum")
})
})
})
Definición del método
Los métodos definen operaciones con ajustes específicos de gRPC:
Method("add", func() {
Description("Add two numbers")
Payload(func() {
Field(1, "a", Int, "First operand")
Field(2, "b", Int, "Second operand")
Required("a", "b")
})
Result(func() {
Field(1, "sum", Int, "Result of addition")
Required("sum")
})
GRPC(func() {
Response(CodeOK)
Response("not_found", CodeNotFound)
Response("invalid_argument", CodeInvalidArgument)
})
})
Tipos de mensajes
Numeración de campos
Utilice las mejores prácticas de Protocol Buffer:
- Números 1-15: Campos frecuentes (codificación de 1 byte)
- Números 16-2047: Campos menos frecuentes (codificación de 2 bytes)
Method("createUser", func() {
Payload(func() {
// Frequently used fields (1-byte encoding)
Field(1, "id", String)
Field(2, "name", String)
Field(3, "email", String)
// Less frequently used fields (2-byte encoding)
Field(16, "preferences", func() {
Field(1, "theme", String)
Field(2, "language", String)
})
})
})
Gestión de metadatos
Enviar campos como metadatos gRPC en lugar del cuerpo del mensaje:
var CreatePayload = Type("CreatePayload", func() {
Field(1, "name", String, "Name of account")
TokenField(2, "token", String, "JWT token")
Field(3, "metadata", String, "Additional info")
})
Method("create", func() {
Payload(CreatePayload)
GRPC(func() {
// Send token in metadata
Metadata(func() {
Attribute("token")
})
// Only include specific fields in message
Message(func() {
Attribute("name")
Attribute("metadata")
})
Response(CodeOK)
})
})
Cabeceras de respuesta y trailers
Method("create", func() {
Result(CreateResult)
GRPC(func() {
Response(func() {
Code(CodeOK)
Headers(func() {
Attribute("id")
})
Trailers(func() {
Attribute("status")
})
})
})
})
Streaming Patterns
Recapitulación del diseño: El streaming se define a nivel de diseño usando
StreamingPayloadyStreamingResult. El DSL es agnóstico al transporte - el mismo diseño funciona tanto para HTTP como para gRPC. Ver DSL Reference: Streaming para patrones de diseño. Esta sección cubre la implementación de streaming específica de gRPC.
gRPC soporta tres patrones de streaming.
Streaming del lado del servidor
El servidor envía múltiples respuestas a una única petición del cliente:
var _ = Service("monitor", func() {
Method("watch", func() {
Description("Stream system metrics")
Payload(func() {
Field(1, "interval", Int, "Sampling interval in seconds")
Required("interval")
})
StreamingResult(func() {
Field(1, "cpu", Float32, "CPU usage percentage")
Field(2, "memory", Float32, "Memory usage percentage")
Required("cpu", "memory")
})
GRPC(func() {
Response(CodeOK)
})
})
})
Implementación del servidor:
func (s *monitorService) Watch(ctx context.Context, p *monitor.WatchPayload, stream monitor.WatchServerStream) error {
ticker := time.NewTicker(time.Duration(p.Interval) * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
metrics := getSystemMetrics()
if err := stream.Send(&monitor.WatchResult{
CPU: metrics.CPU,
Memory: metrics.Memory,
}); err != nil {
return err
}
}
}
}
Streaming del lado del cliente
El cliente envía múltiples peticiones, el servidor envía una única respuesta:
var _ = Service("analytics", func() {
Method("process", func() {
Description("Process stream of analytics events")
StreamingPayload(func() {
Field(1, "event_type", String, "Type of event")
Field(2, "timestamp", String, "Event timestamp")
Field(3, "data", Bytes, "Event data")
Required("event_type", "timestamp", "data")
})
Result(func() {
Field(1, "processed_count", Int64, "Number of events processed")
Required("processed_count")
})
GRPC(func() {
Response(CodeOK)
})
})
})
Implementación del servidor:
func (s *analyticsService) Process(ctx context.Context, stream analytics.ProcessServerStream) error {
var count int64
for {
event, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&analytics.ProcessResult{
ProcessedCount: count,
})
}
if err != nil {
return err
}
if err := processEvent(event); err != nil {
return err
}
count++
}
}
Streaming bidireccional
Tanto el cliente como el servidor envían flujos simultáneamente:
var _ = Service("chat", func() {
Method("connect", func() {
Description("Establish bidirectional chat connection")
StreamingPayload(func() {
Field(1, "message", String, "Chat message")
Field(2, "user_id", String, "User identifier")
Required("message", "user_id")
})
StreamingResult(func() {
Field(1, "message", String, "Chat message")
Field(2, "user_id", String, "User identifier")
Field(3, "timestamp", String, "Message timestamp")
Required("message", "user_id", "timestamp")
})
GRPC(func() {
Response(CodeOK)
})
})
})
Implementación del servidor:
func (s *chatService) Connect(ctx context.Context, stream chat.ConnectServerStream) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
response := &chat.ConnectResult{
Message: msg.Message,
UserID: msg.UserID,
Timestamp: time.Now().Format(time.RFC3339),
}
if err := stream.Send(response); err != nil {
return err
}
}
}
Tratamiento de errores
Recapitulación del diseño: Los errores se definen a nivel de diseño utilizando el DSL Error en el ámbito de la API, servicio o método. Ver DSL Reference: Error Handling para patrones de diseño. Esta sección cubre el mapeo de códigos de estado específico de gRPC.
Códigos de Estado
Mapea errores a códigos de estado gRPC:
Method("divide", func() {
Error("division_by_zero")
Error("invalid_input")
GRPC(func() {
Response(CodeOK)
Response("division_by_zero", CodeInvalidArgument)
Response("invalid_input", CodeInvalidArgument)
})
})
Mapeos de códigos de estado comunes:
| Error de Goa | Código de estado gRPC | Caso de uso |
|---|---|---|
not_found CodeNotFound El recurso no existe | ||
invalid_argument CodeInvalidArgument CodeInvalidArgument Entrada no válida | ||
internal_error | CodeInternal | Error del servidor |
| Faltan credenciales o no son válidas | ||
permission_denied CodePermissionDenied CodePermissionDenied Permisos insuficientes |
Definiciones de error
Definir errores a nivel de servicio o método:
var _ = Service("users", func() {
// Service-level errors
Error("not_found", func() {
Description("User not found")
})
Error("invalid_input")
Method("getUser", func() {
// Method-specific error
Error("profile_incomplete")
GRPC(func() {
Response(CodeOK)
Response("not_found", CodeNotFound)
Response("invalid_input", CodeInvalidArgument)
Response("profile_incomplete", CodeFailedPrecondition)
})
})
})
Devolución de errores
Utilizar constructores de error generados:
func (s *users) CreateUser(ctx context.Context, p *users.CreateUserPayload) (*users.User, error) {
exists, err := s.db.EmailExists(ctx, p.Email)
if err != nil {
return nil, users.MakeDatabaseError(fmt.Errorf("failed to check email: %w", err))
}
if exists {
return nil, users.MakeDuplicateEmail(fmt.Sprintf("email %s is already registered", p.Email))
}
user, err := s.db.CreateUser(ctx, p)
if err != nil {
return nil, users.MakeDatabaseError(fmt.Errorf("failed to create user: %w", err))
}
return user, nil
}
Implementación
Implementación del servidor
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"github.com/yourusername/calc"
gencalc "github.com/yourusername/calc/gen/calc"
genpb "github.com/yourusername/calc/gen/grpc/calc/pb"
gengrpc "github.com/yourusername/calc/gen/grpc/calc/server"
)
func main() {
svc := calc.New()
endpoints := gencalc.NewEndpoints(svc)
svr := grpc.NewServer()
gensvr := gengrpc.New(endpoints, nil)
genpb.RegisterCalcServer(svr, gensvr)
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
log.Println("gRPC server listening on :8080")
svr.Serve(lis)
}
Implementación del cliente
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
gencalc "github.com/yourusername/calc/gen/calc"
genclient "github.com/yourusername/calc/gen/grpc/calc/client"
)
func main() {
conn, err := grpc.Dial("localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
grpcClient := genclient.NewClient(conn)
client := gencalc.NewClient(
grpcClient.Add(),
grpcClient.Multiply(),
)
result, err := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
if err != nil {
log.Fatal(err)
}
log.Printf("1 + 2 = %d", result)
}
Integración del búfer de protocolo
Generación automática
Goa genera automáticamente archivos .proto a partir de su diseño:
syntax = "proto3";
package calc;
service Calc {
rpc Add (AddRequest) returns (AddResponse);
rpc Multiply (MultiplyRequest) returns (MultiplyResponse);
}
message AddRequest {
int64 a = 1;
int64 b = 2;
}
message AddResponse {
int64 result = 1;
}
Configuración Protoc
var _ = Service("calculator", func() {
GRPC(func() {
Meta("protoc:path", "protoc")
Meta("protoc:version", "v3")
Meta("protoc:plugin", "grpc-gateway")
})
})
See Also
- DSL Reference: Streaming - Patrones de streaming a nivel de diseño
- DSL Reference: Error Handling - Definiciones de errores a nivel de diseño
- Guía HTTP - Características del transporte HTTP
- Guía de manejo de errores - Patrones completos de manejo de errores
- Documentación de Clue - Interceptores gRPC para observabilidad
Mejores prácticas
Tratamiento de errores
- Utilizar códigos de estado gRPC apropiados
- Incluir mensajes de error significativos
- Gestionar la cancelación del contexto y los tiempos de espera
Streaming
- Mantener un tamaño razonable de los mensajes
- Implementar un control de flujo adecuado
- Establecer tiempos de espera adecuados
- Manejar EOF y errores con elegancia
Rendimiento
- Utilizar tipos de campo adecuados
- Considerar el tamaño de los mensajes en el diseño
- Utilizar streaming para grandes conjuntos de datos
Versionado
- Planificar la compatibilidad con versiones anteriores
- Utilice los números de campo de forma estratégica
- Considerar el versionado de paquetes
Gestión de recursos
- Gestionar correctamente las conexiones gRPC
- Implementar el apagado graceful
- Limpiar los recursos al cancelar el contexto