Guía gRPC

Complete guide to gRPC transport in Goa - service design, streaming patterns, error handling, and Protocol Buffer integration.

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 .proto generados 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 GoaTipo de Buffer de Protocolo
Intint32
Int32int32
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 StreamingPayload y StreamingResult. 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 GoaCódigo de estado gRPCCaso de uso
not_found CodeNotFound El recurso no existe
invalid_argument CodeInvalidArgument CodeInvalidArgument Entrada no válida
internal_errorCodeInternalError 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


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