Developing gRPC Microservices in Go with Examples
gRPC is a high-performance, open-source universal RPC (Remote Procedure Call) framework developers use to build highly scalable and…
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:
- Go: You can download it from the official Go site: https://golang.org/dl/
- Protoc, the Protocol Buffer compiler: Install it from the official Protobuf GitHub repo: https://github.com/protocolbuffers/protobuf/releases
- Go plugins for the protocol compiler: Install it using
go get google.golang.org/protobuf/cmd/protoc-gen-go google.golang.org/grpc/cmd/protoc-gen-go-grpc
- An editor or IDE of your choice.
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!
Follow me on Medium, LinkedIn, and Twitter.
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
#go #golang #grpc #microservices #api #go #programming #language #softwaredevelopment #coding #software #safety #development #building #architecture #communication