gRPC Guide
Goa provides comprehensive support for building gRPC services through its DSL and code generation. This guide covers service design, streaming patterns, error handling, and implementation.
Overview
Goa’s gRPC support includes:
- Automatic Protocol Buffer Generation:
.protofiles generated from your design - Type Safety: End-to-end type safety from definition to implementation
- Code Generation: Server and client code generated automatically
- Built-in Validation: Request validation based on your design
- Streaming Support: All gRPC streaming patterns supported
- Error Handling: Comprehensive error handling with status code mapping
Type Mapping
| Goa Type | Protocol Buffer Type |
|---|---|
| Int | int32 |
| Int32 | int32 |
| Int64 | int64 |
| UInt | uint32 |
| UInt32 | uint32 |
| UInt64 | uint64 |
| Float32 | float |
| Float64 | double |
| String | string |
| Boolean | bool |
| Bytes | bytes |
| ArrayOf | repeated |
| MapOf | map |
Service Design
Basic Service Structure
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")
})
})
})
Method Definition
Methods define operations with gRPC-specific settings:
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)
})
})
Message Types
Field Numbering
Use Protocol Buffer best practices:
- Numbers 1-15: Frequently occurring fields (1-byte encoding)
- Numbers 16-2047: Less frequent fields (2-byte encoding)
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)
})
})
})
Metadata Handling
Send fields as gRPC metadata instead of message body:
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)
})
})
Response Headers and Trailers
Method("create", func() {
Result(CreateResult)
GRPC(func() {
Response(func() {
Code(CodeOK)
Headers(func() {
Attribute("id")
})
Trailers(func() {
Attribute("status")
})
})
})
})
Streaming Patterns
Design Recap: Streaming is defined at the design level using
StreamingPayloadandStreamingResult. The DSL is transport-agnostic — the same design works for both HTTP and gRPC. See DSL Reference: Streaming for design patterns. This section covers gRPC-specific streaming implementation.
gRPC supports three streaming patterns.
Server-Side Streaming
Server sends multiple responses to a single client request:
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)
})
})
})
Server implementation:
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
}
}
}
}
Client-Side Streaming
Client sends multiple requests, server sends single response:
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)
})
})
})
Server implementation:
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++
}
}
Bidirectional Streaming
Both client and server send streams simultaneously:
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)
})
})
})
Server implementation:
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
}
}
}
Error Handling
Design Recap: Errors are defined at the design level using the
ErrorDSL at API, service, or method scope. See DSL Reference: Error Handling for design patterns. This section covers gRPC-specific status code mapping.
Status Codes
Map errors to gRPC status codes:
Method("divide", func() {
Error("division_by_zero")
Error("invalid_input")
GRPC(func() {
Response(CodeOK)
Response("division_by_zero", CodeInvalidArgument)
Response("invalid_input", CodeInvalidArgument)
})
})
Common status code mappings:
| Goa Error | gRPC Status Code | Use Case |
|---|---|---|
not_found | CodeNotFound | Resource doesn’t exist |
invalid_argument | CodeInvalidArgument | Invalid input |
internal_error | CodeInternal | Server error |
unauthenticated | CodeUnauthenticated | Missing/invalid credentials |
permission_denied | CodePermissionDenied | Insufficient permissions |
Error Definitions
Define errors at service or method level:
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)
})
})
})
Returning Errors
Use generated error constructors:
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
}
Implementation
Server Implementation
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)
}
Client Implementation
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)
}
Protocol Buffer Integration
Automatic Generation
Goa automatically generates .proto files from your design:
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;
}
Protoc Configuration
var _ = Service("calculator", func() {
GRPC(func() {
Meta("protoc:path", "protoc")
Meta("protoc:version", "v3")
Meta("protoc:plugin", "grpc-gateway")
})
})
See Also
- DSL Reference: Streaming — Design-level streaming patterns
- DSL Reference: Error Handling — Design-level error definitions
- HTTP Guide — HTTP transport features
- Error Handling Guide — Complete error handling patterns
- Clue Documentation — gRPC interceptors for observability
Best Practices
Error Handling
- Use appropriate gRPC status codes
- Include meaningful error messages
- Handle context cancellation and timeouts
Streaming
- Keep message sizes reasonable
- Implement proper flow control
- Set appropriate timeouts
- Handle EOF and errors gracefully
Performance
- Use appropriate field types
- Consider message size in design
- Use streaming for large datasets
Versioning
- Plan for backward compatibility
- Use field numbers strategically
- Consider package versioning
Resource Management
- Properly manage gRPC connections
- Implement graceful shutdown
- Clean up resources on context cancellation