Andre de Oliveira.

gRPC in Go - Part 2: Building the Server

Cover Image for gRPC in Go - Part 2: Building the Server
Andre
Andre

In Part 1, we set up our project and defined our Protocol Buffers. Now, let's implement the server side of our e-commerce application. We'll create a fully functional gRPC server that handles product-related operations.

Implementing the Product Service

First, let's create a struct that implements our ProductServiceServer interface. Create a new file called server/product_service.go:

package server

import (
    "context"
    "fmt"
    pb "github.com/andredeloliveira/ecommerce-grpc/ecommerce-proto"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

type ProductService struct {
    pb.UnimplementedProductServiceServer
    products map[string]*pb.Product // Simple in-memory storage
}

func NewProductService() *ProductService {
    // Initialize with some sample data
    products := map[string]*pb.Product{
        "1": {
            Id:          "1",
            Name:        "Gaming Laptop",
            Description: "High-performance gaming laptop",
            Price:       1299.99,
            Stock:       10,
        },
        "2": {
            Id:          "2",
            Name:        "Wireless Mouse",
            Description: "Ergonomic wireless mouse",
            Price:       49.99,
            Stock:       50,
        },
    }
    
    return &ProductService{products: products}
}

Implementing Service Methods

Now, let's implement the required methods:

func (s *ProductService) GetProduct(ctx context.Context, req *pb.ProductID) (*pb.Product, error) {
    // Add basic validation
    if req.Id == "" {
        return nil, status.Error(codes.InvalidArgument, "product ID cannot be empty")
    }

    // Look up the product
    product, exists := s.products[req.Id]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "product with ID %s not found", req.Id)
    }

    return product, nil
}

func (s *ProductService) ListProducts(ctx context.Context, req *pb.ProductListRequest) (*pb.ProductList, error) {
    // Add basic validation
    if req.Page < 1 {
        return nil, status.Error(codes.InvalidArgument, "page must be greater than 0")
    }
    if req.Limit < 1 {
        return nil, status.Error(codes.InvalidArgument, "limit must be greater than 0")
    }

    // Convert map to slice for pagination
    products := make([]*pb.Product, 0, len(s.products))
    for _, product := range s.products {
        products = append(products, product)
    }

    // Calculate pagination
    startIndex := (int(req.Page) - 1) * int(req.Limit)
    endIndex := startIndex + int(req.Limit)
    if startIndex >= len(products) {
        return &pb.ProductList{Products: []*pb.Product{}, Total: int32(len(products))}, nil
    }
    if endIndex > len(products) {
        endIndex = len(products)
    }

    return &pb.ProductList{
        Products: products[startIndex:endIndex],
        Total:    int32(len(products)),
    }, nil
}

Setting Up the gRPC Server

Create a new file called main.go to set up and run the server:

package main

import (
    "log"
    "net"
    
    pb "github.com/andredeloliveira/ecommerce-grpc/ecommerce-proto"
    "github.com/andredeloliveira/ecommerce-grpc/server"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // Create a new gRPC server
    s := grpc.NewServer()

    // Register our service
    productService := server.NewProductService()
    pb.RegisterProductServiceServer(s, productService)

    // Register reflection service on gRPC server
    reflection.Register(s)

    log.Printf("Server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Adding Middleware for Logging

Let's add some middleware to log incoming requests:

func LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    start := time.Now()
    
    // Execute the handler
    h, err := handler(ctx, req)
    
    // Log the request
    log.Printf(
        "Request - Method:%s\tDuration:%s\tError:%v",
        info.FullMethod,
        time.Since(start),
        err,
    )
    
    return h, err
}

// Update the main function to use the interceptor
func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    // Create a new gRPC server with the logging interceptor
    s := grpc.NewServer(
        grpc.UnaryInterceptor(LoggingInterceptor),
    )

    // ... rest of the main function
}

Testing the Server

You can test your server using tools like grpcurl. First, install it:

go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

Then test your endpoints:

# List all available services
grpcurl -plaintext localhost:50051 list

# Get a specific product
grpcurl -plaintext -d '{"id": "1"}' localhost:50051 ecommerce.ProductService/GetProduct

# List products
grpcurl -plaintext -d '{"page": 1, "limit": 10}' localhost:50051 ecommerce.ProductService/ListProducts

Next Steps

In Part 3, we'll build the client side of our application, including:

  • Creating a gRPC client
  • Implementing retry logic
  • Handling timeouts and cancellation
  • Best practices for client-side implementation

Stay tuned for the final part of our series!