gRPC Service Example
This example demonstrates how to build a gRPC-only service using the Swit framework with Protocol Buffers. It showcases gRPC service registration, Protocol Buffer message handling, server reflection, and health service integration.
Overview
The gRPC Service example (examples/grpc-service/
) provides:
- gRPC-only service - Focused on Protocol Buffer-based RPC functionality
- Protocol Buffer integration - Using generated Go code from .proto definitions
- Server reflection - gRPC reflection for development and debugging
- Health service - Standard gRPC health checking protocol
- Keepalive configuration - Connection management and optimization
Key Features
Service Architecture
- Implements
BusinessServiceRegistrar
for framework integration - Registers gRPC services with proper server configuration
- Uses Protocol Buffer definitions for type-safe communication
- Demonstrates proper gRPC service lifecycle management
gRPC Services
- GreeterService - Implements
swit.interaction.v1.GreeterService
- SayHello RPC method - Takes name parameter and returns greeting message
- Health Service - Standard gRPC health check integration
- Reflection Service - For development tools and debugging
Advanced gRPC Features
- Message size limits (4MB default)
- Keepalive parameters for connection management
- Server-side validation and error handling
- Proper gRPC status codes and error responses
Code Structure
Main Service Implementation
go
// GreeterService implements the ServiceRegistrar interface
type GreeterService struct {
name string
}
func (s *GreeterService) RegisterServices(registry server.BusinessServiceRegistry) error {
// Register gRPC service
grpcService := &GreeterGRPCService{serviceName: s.name}
if err := registry.RegisterBusinessGRPCService(grpcService); err != nil {
return fmt.Errorf("failed to register gRPC service: %w", err)
}
// Register health check
healthCheck := &GreeterHealthCheck{serviceName: s.name}
if err := registry.RegisterBusinessHealthCheck(healthCheck); err != nil {
return fmt.Errorf("failed to register health check: %w", err)
}
return nil
}
gRPC Service Implementation
go
type GreeterGRPCService struct {
serviceName string
interaction.UnimplementedGreeterServiceServer
}
func (s *GreeterGRPCService) RegisterGRPC(server interface{}) error {
grpcServer := server.(*grpc.Server)
interaction.RegisterGreeterServiceServer(grpcServer, s)
return nil
}
func (s *GreeterGRPCService) SayHello(ctx context.Context, req *interaction.SayHelloRequest) (*interaction.SayHelloResponse, error) {
// Validate request
if req.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "name cannot be empty")
}
// Create response
response := &interaction.SayHelloResponse{
Message: fmt.Sprintf("Hello, %s!", req.GetName()),
}
return response, nil
}
gRPC Configuration
go
config := &server.ServerConfig{
ServiceName: "grpc-greeter-service",
HTTP: server.HTTPConfig{
Enabled: false, // gRPC-only service
},
GRPC: server.GRPCConfig{
Port: getEnv("GRPC_PORT", "9090"),
EnableReflection: true,
EnableHealthService: true,
Enabled: true,
MaxRecvMsgSize: 4 * 1024 * 1024, // 4MB
MaxSendMsgSize: 4 * 1024 * 1024, // 4MB
KeepaliveParams: server.GRPCKeepaliveParams{
MaxConnectionIdle: 15 * time.Minute,
MaxConnectionAge: 30 * time.Minute,
MaxConnectionAgeGrace: 5 * time.Minute,
Time: 5 * time.Minute,
Timeout: 1 * time.Minute,
},
KeepalivePolicy: server.GRPCKeepalivePolicy{
MinTime: 5 * time.Minute,
PermitWithoutStream: false,
},
},
}
Protocol Buffer Definition
The service uses Protocol Buffer definitions from api/proto/swit/interaction/v1/
:
protobuf
syntax = "proto3";
package swit.interaction.v1;
service GreeterService {
rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
}
message SayHelloRequest {
string name = 1;
}
message SayHelloResponse {
string message = 1;
}
Running the Example
Prerequisites
- Go 1.24+ installed
- gRPC tools installed (for testing)
- Framework dependencies available
Quick Start
Navigate to the example directory:
bashcd examples/grpc-service
Run the service:
bashgo run main.go
Test with grpcurl:
bash# Install grpcurl if not available go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest # Test SayHello method grpcurl -plaintext -d '{"name": "Alice"}' \ localhost:9090 \ swit.interaction.v1.GreeterService/SayHello # Check health service grpcurl -plaintext localhost:9090 grpc.health.v1.Health/Check # List available services (reflection) grpcurl -plaintext localhost:9090 list
Environment Configuration
bash
# Set custom gRPC port
export GRPC_PORT=9999
# Enable service discovery
export DISCOVERY_ENABLED=true
export CONSUL_ADDRESS=localhost:8500
# Run with custom configuration
go run main.go
Expected Responses
SayHello RPC:
json
{
"message": "Hello, Alice!"
}
Health Check:
json
{
"status": "SERVING"
}
Service List:
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
swit.interaction.v1.GreeterService
Development Patterns
Adding New RPC Methods
Update Protocol Buffer definition:
protobufservice GreeterService { rpc SayHello(SayHelloRequest) returns (SayHelloResponse); rpc SayGoodbye(SayGoodbyeRequest) returns (SayGoodbyeResponse); } message SayGoodbyeRequest { string name = 1; } message SayGoodbyeResponse { string message = 1; }
Regenerate Go code:
bashmake proto
Implement method:
gofunc (s *GreeterGRPCService) SayGoodbye(ctx context.Context, req *interaction.SayGoodbyeRequest) (*interaction.SayGoodbyeResponse, error) { if req.GetName() == "" { return nil, status.Error(codes.InvalidArgument, "name cannot be empty") } response := &interaction.SayGoodbyeResponse{ Message: fmt.Sprintf("Goodbye, %s!", req.GetName()), } return response, nil }
Error Handling Patterns
go
func (s *GreeterGRPCService) ValidatedMethod(ctx context.Context, req *SomeRequest) (*SomeResponse, error) {
// Input validation
if req.GetField() == "" {
return nil, status.Error(codes.InvalidArgument, "field is required")
}
// Business logic with error handling
result, err := s.processRequest(req)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
return nil, status.Error(codes.NotFound, "resource not found")
case errors.Is(err, ErrPermissionDenied):
return nil, status.Error(codes.PermissionDenied, "access denied")
default:
return nil, status.Error(codes.Internal, "internal error")
}
}
return result, nil
}
Context and Timeout Handling
go
func (s *GreeterGRPCService) LongRunningMethod(ctx context.Context, req *SomeRequest) (*SomeResponse, error) {
// Check for cancellation
select {
case <-ctx.Done():
return nil, status.Error(codes.Canceled, "request canceled")
default:
}
// Set timeout for external calls
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := s.externalService.Call(timeoutCtx, req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, status.Error(codes.DeadlineExceeded, "operation timed out")
}
return nil, status.Error(codes.Internal, err.Error())
}
return result, nil
}
Testing gRPC Services
Unit Testing
go
func TestGreeterService_SayHello(t *testing.T) {
service := &GreeterGRPCService{serviceName: "test-service"}
tests := []struct {
name string
request *interaction.SayHelloRequest
want string
wantErr codes.Code
}{
{
name: "valid request",
request: &interaction.SayHelloRequest{Name: "Alice"},
want: "Hello, Alice!",
wantErr: codes.OK,
},
{
name: "empty name",
request: &interaction.SayHelloRequest{Name: ""},
want: "",
wantErr: codes.InvalidArgument,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := service.SayHello(context.Background(), tt.request)
if tt.wantErr != codes.OK {
assert.Error(t, err)
st, ok := status.FromError(err)
assert.True(t, ok)
assert.Equal(t, tt.wantErr, st.Code())
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, resp.GetMessage())
}
})
}
}
Integration Testing
go
func TestGRPCServer(t *testing.T) {
// Start test server
config := &server.ServerConfig{
GRPC: server.GRPCConfig{
Port: "0", // Dynamic port
Enabled: true,
EnableReflection: true,
},
}
service := NewGreeterService("test")
srv, err := server.NewBusinessServerCore(config, service, nil)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = srv.Start(ctx)
require.NoError(t, err)
defer srv.Shutdown()
// Create client connection
conn, err := grpc.DialContext(ctx, srv.GetGRPCAddress(),
grpc.WithInsecure(),
grpc.WithBlock(),
)
require.NoError(t, err)
defer conn.Close()
// Test service
client := interaction.NewGreeterServiceClient(conn)
resp, err := client.SayHello(ctx, &interaction.SayHelloRequest{
Name: "TestUser",
})
assert.NoError(t, err)
assert.Equal(t, "Hello, TestUser!", resp.GetMessage())
}
Best Practices Demonstrated
- Protocol Buffer Design - Clean message definitions with proper field naming
- Error Handling - Appropriate gRPC status codes for different error conditions
- Input Validation - Server-side validation with meaningful error messages
- Context Usage - Proper context propagation and timeout handling
- Service Registration - Clean integration with framework service registry
Next Steps
After understanding this gRPC example:
- Combine with HTTP - See
full-featured-service
for dual HTTP/gRPC transport - Advanced Features - Explore streaming RPCs, interceptors, and metadata
- Production Setup - Add authentication, rate limiting, and monitoring
- Client Libraries - Generate client code for other languages
This example provides a solid foundation for building gRPC-based microservices with the Swit framework using modern Protocol Buffer patterns.