gRPC in Go - Part 3: Creating the Client
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!