Developing gRPC Microservices in Go with?Examples
Luis Soares, M.Sc.
Lead Software Engineer | Blockchain & ZK Protocol Engineer | ?? Rust | C++ | Web3 | Solidity | Golang | Cryptography | Author
gRPC is a high-performance, open-source universal RPC (Remote Procedure Call) framework developers use to build highly scalable and distributed systems. It uses Protobuf (protocol buffers) as its interface definition language, which allows for a reliable way to define services and message types.
Microservices architecture is a way of designing software applications as suites of independently deployable services. It's a popular architecture for complex, evolving systems because it allows for scaling and allows teams to work on different services concurrently.
This article discusses developing gRPC microservices using the Go programming language.
Setting Up Your Environment
Before we start, you'll need the following installed on your machine:
Defining the?Service
Firstly, we need to define the service. Create a new directory for your project and create a new 'proto' file in it. Let's call it. example.proto.
syntax = "proto3";
package example;
// Define your service
service ExampleService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
// Define your message types
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 1;
}
This code defines a simple service called ExampleService with a single RPC method SayHello, which takes a HelloRequest message and returns a HelloResponse message.
Generating the?Code
Next, we generate the Go code from the service definition. Run this command:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative example.proto
This command tells the Protobuf compiler to generate Go code with the gRPC plugin. The generated code will include methods for creating a server and a client and the necessary data structures.
Implementing the?Service
Now let's implement the service. Create a new Go file, server.go, and write the following code:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"example.com/example"
)
type server struct {
example.UnimplementedExampleServiceServer
}
func (s *server) SayHello(ctx context.Context, req *example.HelloRequest) (*example.HelloResponse, error) {
return &example.HelloResponse{Greeting: "Hello, " + req.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
example.RegisterExampleServiceServer(grpcServer, &server{})
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
In this file, we create a server struct that embeds the UnimplementedExampleServiceServer. This struct comes from the generated code and contains all the service methods. By embedding it in our struct, we can ensure our server fulfils the ExampleServiceServer interface even if we don't implement all the methods.
The SayHello function implements our gRPC method. It receives a HelloRequest and returns a HelloResponse.
In the main function, we start a gRPC server on port 50051 and register our service.
Creating a?Client
To interact with our service, we need a client. Create a new Go file, client.go, with the following code:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"example.com/example"
)
func main() {
conn, err := grpc.Dial(":50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := example.NewExampleServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := client.SayHello(ctx, &example.HelloRequest{Name: "World"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetGreeting())
}
In the main function, we first create a connection to the server. We then create a client with the connection. Finally, we make a call to the SayHello method.
Running the Service and?Client
First, run the server:
领英推荐
go run server.go
Then, in a new terminal window, run the client:
go run client.go
You should see a "Hello, World" message in the client's output.
Error Handling
You'll want to add error handling to your gRPC service in a real-world application. gRPC uses the status package to send error details from the server to the client. Let's modify our server to return an error if the client does not provide a name.
import (
// ...
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
)
func (s *server) SayHello(ctx context.Context, req *example.HelloRequest) (*example.HelloResponse, error) {
if req.Name == "" {
return nil, status.Errorf(
codes.InvalidArgument,
"Name can't be empty",
)
}
return &example.HelloResponse{Greeting: "Hello, " + req.GetName()}, nil
}
And in the client, we handle the error:
r, err := client.SayHello(ctx, &example.HelloRequest{Name: ""})
if err != nil {
statusErr, ok := status.FromError(err)
if ok {
log.Printf("Error: %v, %v", statusErr.Code(), statusErr.Message())
return
} else {
log.Fatalf("could not greet: %v", err)
}
}
When you run the client with an empty name, you'll get an error message from the server.
Inter-service Communication
Microservices often need to communicate with each other. They can do this via gRPC as well. To demonstrate this, let's imagine we have a UserService and an OrderService. The UserService needs to request data from the OrderService.
First, define both services in the proto file:
service UserService {
rpc GetUserOrders (UserRequest) returns (UserOrdersResponse);
}
service OrderService {
rpc GetOrders (OrderRequest) returns (OrderResponse);
}
message UserRequest {
string userId = 1;
}
message UserOrdersResponse {
repeated Order orders = 1;
}
message OrderRequest {
string userId = 1;
}
message OrderResponse {
repeated Order orders = 1;
}
message Order {
string orderId = 1;
string userId = 2;
string details = 3;
}
The UserService would have a client to the OrderService:
type userServiceServer struct {
OrderServiceClient example.OrderServiceClient
}
func (s *userServiceServer) GetUserOrders(ctx context.Context, req *example.UserRequest) (*example.UserOrdersResponse, error) {
orders, err := s.OrderServiceClient.GetOrders(ctx, &example.OrderRequest{UserId: req.UserId})
if err != nil {
return nil, err
}
return &example.UserOrdersResponse{Orders: orders.Orders}, nil
}
The OrderService would then process the order retrieval:
type orderServiceServer struct {
// ... some database or data source
}
func (s *orderServiceServer) GetOrders(ctx context.Context, req *example.OrderRequest) (*example.OrderResponse, error) {
// ... retrieve orders for the user from the database
}
RPC provides a robust and efficient method for inter-service communication in a microservice architecture. It's especially effective with a statically typed language like Go, ensuring type safety between the client and server.
In this article, we've covered the basics of setting up a gRPC service in Go, including error handling and inter-service communication. With this foundation, you should be able to create and deploy your microservices with Go and gRPC.
Stay tuned, and happy coding!
Visit my Blog for more articles, news, and software engineering stuff!
Check out my most recent book — Application Security: A Quick Reference to the Building Blocks of Secure Software.
All the best,
Luis Soares
CTO | Head of Engineering | Blockchain Engineer | Solidity | Rust | Smart Contracts | Web3 | Cyber Security
Applicatiespecialist bij Quion
2 个月When you mentioned: ".. a way of designing software applications as suites of independetly deployable services .." It gave me an idea like this could be a chain of the blockchain.. curious if this actually work out. ?? ???
Thanks for Sharing! ?? Luis Soares, M.Sc.