Andre de Oliveira.

gRPC in Go - Part 3: Creating the Client

Cover Image for gRPC in Go - Part 3: Creating the Client
Andre
Andre

In this final part of our series, we'll implement a robust gRPC client for our e-commerce application. We'll cover best practices, error handling, and how to make our client production-ready.

Creating the Client Package

First, let's create a new directory for our client code and implement a client wrapper. Create client/product_client.go:

package client

import (
    "context"
    "time"

    pb "github.com/andredeloliveira/ecommerce-grpc/ecommerce-proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

type ProductClient struct {
    client     pb.ProductServiceClient
    conn       *grpc.ClientConn
    timeout    time.Duration
}

func NewProductClient(address string) (*ProductClient, error) {
    // Set up connection with retry options
    conn, err := grpc.Dial(
        address,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithBlock(),
    )
    if err != nil {
        return nil, err
    }

    return &ProductClient{
        client:  pb.NewProductServiceClient(conn),
        conn:    conn,
        timeout: 5 * time.Second, // Default timeout
    }, nil
}

// Close closes the client connection
func (c *ProductClient) Close() error {
    return c.conn.Close()
}

Implementing Client Methods

Let's add methods to interact with our service:

// GetProduct retrieves a product by ID
func (c *ProductClient) GetProduct(ctx context.Context, id string) (*pb.Product, error) {
    ctx, cancel := context.WithTimeout(ctx, c.timeout)
    defer cancel()

    return c.client.GetProduct(ctx, &pb.ProductID{Id: id})
}

// ListProducts retrieves a list of products with pagination
func (c *ProductClient) ListProducts(ctx context.Context, page, limit int32) (*pb.ProductList, error) {
    ctx, cancel := context.WithTimeout(ctx, c.timeout)
    defer cancel()

    return c.client.ListProducts(ctx, &pb.ProductListRequest{
        Page:  page,
        Limit: limit,
    })
}

Adding Retry Logic

Let's implement retry logic for our client operations:

package client

import (
    "context"
    "time"

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

type RetryConfig struct {
    MaxRetries  int
    BaseDelay   time.Duration
    MaxDelay    time.Duration
}

func DefaultRetryConfig() RetryConfig {
    return RetryConfig{
        MaxRetries: 3,
        BaseDelay:  100 * time.Millisecond,
        MaxDelay:   2 * time.Second,
    }
}

func (c *ProductClient) withRetry(ctx context.Context, operation func(context.Context) error) error {
    config := DefaultRetryConfig()
    
    var lastErr error
    for attempt := 0; attempt < config.MaxRetries; attempt++ {
        if err := operation(ctx); err != nil {
            lastErr = err
            
            // Check if the error is retryable
            if !isRetryable(err) {
                return err
            }
            
            // Calculate backoff delay
            delay := calculateBackoff(attempt, config)
            
            // Check if context is cancelled before sleeping
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(delay):
                continue
            }
        }
        return nil
    }
    return lastErr
}

func isRetryable(err error) bool {
    code := status.Code(err)
    switch code {
    case codes.Unavailable,
         codes.DeadlineExceeded,
         codes.ResourceExhausted:
        return true
    default:
        return false
    }
}

func calculateBackoff(attempt int, config RetryConfig) time.Duration {
    delay := config.BaseDelay * time.Duration(1<<uint(attempt))
    if delay > config.MaxDelay {
        delay = config.MaxDelay
    }
    return delay
}

Example Usage

Here's how to use our client in a main application:

package client

import (
    "context"
    "log"
    "time"

    "github.com/andredeloliveira/ecommerce-grpc/client"
)

func main() {
    // Create a new client
    productClient, err := client.NewProductClient("localhost:50051")
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }
    defer productClient.Close()

    // Create a context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Get a specific product
    product, err := productClient.GetProduct(ctx, "1")
    if err != nil {
        log.Printf("Error getting product: %v", err)
    } else {
        log.Printf("Got product: %+v", product)
    }

    // List products
    productList, err := productClient.ListProducts(ctx, 1, 10)
    if err != nil {
        log.Printf("Error listing products: %v", err)
    } else {
        log.Printf("Got %d products", len(productList.Products))
        for _, p := range productList.Products {
            log.Printf("- %s: %s ($%.2f)", p.Id, p.Name, p.Price)
        }
    }
}

Conclusion

We've built a robust gRPC client with:

  • Retry logic with exponential backoff
  • Proper error handling

This completes our series on gRPC in Go. You now have a fully functional gRPC-based e-commerce service with both server and client implementations. Remember to always consider your specific use case when implementing these patterns and adjust accordingly.

Happy coding!