grpc

gRPC framework best practices and implementation

SKILL.md

GitHub

What I do

  • Define Protocol Buffer schemas
  • Implement gRPC services
  • Handle streaming
  • Implement authentication
  • Handle errors properly
  • Manage versioning
  • Test gRPC services
  • Scale gRPC deployments

When to use me

When implementing gRPC services or working with Protocol Buffers.

Protocol Buffer Definition

syntax = "proto3";

package user.v1;

option go_package = "github.com/company/api/user/v1";

import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";


// Service definition
service UserService {
  // Unary RPC
  rpc GetUser(GetUserRequest) returns (User);
  
  // Server streaming
  rpc ListUsers(ListUsersRequest) returns (stream User);
  
  // Client streaming
  rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
  
  // Bidirectional streaming
  rpc StreamUserUpdates(stream UserUpdateRequest) returns (stream User);
  
  // Health check
  rpc HealthCheck(google.protobuf.Empty) returns (HealthCheckResponse);
}


// Message types
message GetUserRequest {
  string user_id = 1;
}

message User {
  string id = 1;
  string email = 2;
  string name = 3;
  UserStatus status = 4;
  google.protobuf.Timestamp created_at = 5;
  google.protobuf.Timestamp updated_at = 6;
}

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_INACTIVE = 2;
  USER_STATUS_SUSPENDED = 3;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
  UserStatus status_filter = 3;
}

message CreateUserRequest {
  string email = 1;
  string name = 2;
  string password = 3;
}

message CreateUsersResponse {
  repeated User users = 1;
  int32 failed_count = 2;
}

message UserUpdateRequest {
  string user_id = 1;
  string name = 2;
}

message HealthCheckResponse {
  bool healthy = 1;
  string version = 2;
}

Go gRPC Server

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/types/known/timestamppb"

    pb "github.com/company/api/user/v1"
)

type UserServer struct {
    pb.UnimplementedUserServiceServer
    userStore UserStore
}

type UserStore interface {
    GetUser(ctx context.Context, id string) (*pb.User, error)
    ListUsers(ctx context.Context, filter *pb.UserStatus, limit int) ([]*pb.User, error)
    CreateUser(ctx context.Context, user *pb.User) error
}

func NewUserServer(store UserStore) *UserServer {
    return &UserServer{userStore: store}
}

func (s *UserServer) GetUser(
    ctx context.Context,
    req *pb.GetUserRequest,
) (*pb.User, error) {
    // Extract metadata for logging
    md, ok := metadata.FromIncomingContext(ctx)
    if ok {
        log.Printf("GetUser request: user_id=%s, from=%v", req.UserId, md.Get("x-forwarded-for"))
    }

    // Validate request
    if req.UserId == "" {
        return nil, status.Error(codes.InvalidArgument, "user_id is required")
    }

    // Fetch user
    user, err := s.userStore.GetUser(ctx, req.UserId)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "user not found: %s", err)
    }

    return user, nil
}

func (s *UserServer) ListUsers(
    req *pb.ListUsersRequest,
    stream pb.UserService_ListUsersServer,
) error {
    ctx := stream.Context()

    // Parse pagination
    pageSize := int(req.PageSize)
    if pageSize <= 0 {
        pageSize = 10
    }
    if pageSize > 100 {
        pageSize = 100
    }

    users, err := s.userStore.ListUsers(ctx, req.StatusFilter, pageSize)
    if err != nil {
        return status.Errorf(codes.Internal, "failed to list users: %v", err)
    }

    // Stream users to client
    for _, user := range users {
        if err := stream.Send(user); err != nil {
            return status.Errorf(codes.Internal, "failed to send user: %v", err)
        }
    }

    return nil
}

func (s *UserService) CreateUsers(
    stream pb.UserService_CreateUsersServer,
) error {
    ctx := stream.Context()
    var users []*pb.User
    var failedCount int32

    for {
        req, err := stream.Recv()
        if err != nil {
            if err.Error() == "EOF" {
                break
            }
            return status.Errorf(codes.Internal, "failed to receive request: %v", err)
        }

        user := &pb.User{
            Email:     req.Email,
            Name:      req.Name,
            Status:    pb.UserStatus_USER_STATUS_ACTIVE,
            CreatedAt: timestamppb.Now(),
        }

        if err := s.userStore.CreateUser(ctx, user); err != nil {
            failedCount++
            log.Printf("Failed to create user: %v", err)
            continue
        }

        users = append(users, user)
    }

    return stream.SendAndClose(&pb.CreateUsersResponse{
        Users:       users,
        FailedCount: failedCount,
    })
}

func (s *UserServer) StreamUserUpdates(
    stream pb.UserService_StreamUserUpdatesServer,
) error {
    ctx := stream.Context()

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            req, err := stream.Recv()
            if err != nil {
                return err
            }

            user, err := s.userStore.GetUser(ctx, req.UserId)
            if err != nil {
                continue
            }

            if err := stream.Send(user); err != nil {
                return err
            }
        }
    }
}

// Server reflection for debugging
func init() {
    // Enable reflection
    // reflection.Register(grpcServer)
}

gRPC Client

package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"

    pb "github.com/company/api/user/v1"
)

type UserClient struct {
    conn   *grpc.ClientConn
    client pb.UserServiceClient
}

func NewUserClient(addr string) (*UserClient, error) {
    conn, err := grpc.Dial(
        addr,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithUnaryInterceptor(loggingInterceptor),
        grpc.WithStreamInterceptor(streamLoggingInterceptor),
    )
    if err != nil {
        return nil, err
    }

    return &UserClient{
        conn:   conn,
        client: pb.NewUserServiceClient(conn),
    }, nil
}

func (c *UserClient) GetUser(ctx context.Context, userId string) (*pb.User, error) {
    // Add metadata for tracing
    ctx = metadata.AppendToOutgoingContext(ctx, "x-request-id", "req-123")

    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    return c.client.GetUser(ctx, &pb.GetUserRequest{
        UserId: userId,
    })
}

func (c *UserClient) ListActiveUsers(ctx context.Context) ([]*pb.User, error) {
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    stream, err := c.client.ListUsers(ctx, &pb.ListUsersRequest{
        PageSize:  100,
        StatusFilter: pb.UserStatus_USER_STATUS_ACTIVE,
    })
    if err != nil {
        return nil, err
    }

    var users []*pb.User
    for {
        user, err := stream.Recv()
        if err != nil {
            if err.Error() == "EOF" {
                break
            }
            return nil, err
        }
        users = append(users, user)
    }

    return users, nil
}

func (c *UserClient) Close() error {
    return c.conn.Close()
}

// Interceptors
func loggingInterceptor(
    ctx context.Context,
    method string,
    req, reply interface{},
    cc *grpc.ClientConn,
    invoker grpc.UnaryInvoker,
    opts ...grpc.CallOption,
) error {
    start := time.Now()
    
    err := invoker(ctx, method, req, reply, cc, opts...)
    
    log.Printf("gRPC call: %s, duration: %v, error: %v",
        method, time.Since(start), err)
    
    return err
}

func streamLoggingInterceptor(
    ctx context.Context,
    desc *grpc.StreamDesc,
    cc *grpc.ClientConn,
    method string,
    stream grpc.ClientStream,
    opts ...grpc.CallOption,
) error {
    start := time.Now()
    
    err := stream.RecvMsg(nil)
    
    log.Printf("gRPC stream: %s, duration: %v, error: %v",
        method, time.Since(start), err)
    
    return err
}

Error Handling

package status

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// Custom error codes
const (
    ErrCodeUserNotFound = "USER_NOT_FOUND"
    ErrCodeInvalidInput = "INVALID_INPUT"
    ErrCodeUnauthorized = "UNAUTHORIZED"
)

func UserNotFound(id string) error {
    return status.Errorf(codes.NotFound, "user not found: %s", id)
}

func InvalidInput(field, reason string) error {
    return status.Errorf(codes.InvalidArgument, "invalid %s: %s", field, reason)
}

func Unauthorized(reason string) error {
    return status.Errorf(codes.Unauthenticated, "unauthorized: %s", reason)
}

gRPC Best Practices

1. Use Protocol Buffers for schema
   - Define contracts explicitly
   - Enable backwards compatibility

2. Handle errors properly
   - Use gRPC status codes
   - Include error details

3. Implement interceptors
   - Logging, auth, metrics

4. Use streaming appropriately
   - Server streaming for lists
   - Bidirectional for real-time

5. Set timeouts
   - Always use context with timeout

6. Enable reflection
   - For debugging and tools

7. Version your APIs
   - Include version in package name
   - Support multiple versions

8. Use proper authentication
   - Token-based or mTLS

9. Monitor and trace
   - Add interceptors for metrics
   - Use OpenTelemetry

10. Test thoroughly
    - Unit tests with mock stores
    - Integration tests