How to use gRPC in Go
gRPC: The high-performance communication protocol revolutionizing distributed systems
In the realm of modern distributed systems, efficient and reliable communication between microservices is crucial. Traditional protocols like REST have served well, but as systems grow in complexity and demand real-time communication, gRPC emerges as a powerful solution. In this article, we'll delve into gRPC, understand its benefits, and explore a practical example in Go.
What is gRPC?
gRPC is an open-source high-performance Remote Procedure Call (RPC) framework developed by Google. It enables seamless communication between services running on different platforms and languages. gRPC is based on HTTP/2, which provides multiplexing, stream prioritization, and header compression, leading to faster and more efficient communication compared to REST.
Key Features and Advantages of gRPC
Protocol Buffers (Protobuf): gRPC uses Protocol Buffers as the default data serialization mechanism. Protobuf offers efficient binary serialization, reducing payload size and facilitating faster data transmission.
Bidirectional Streaming: Unlike REST, where the client sends a request and waits for a response, gRPC supports bidirectional streaming. This means both the client and server can send multiple messages asynchronously over a single connection, ideal for real-time applications.
Code Generation: gRPC generates client and server code in various languages (including Go, Java, Python, and more) from a single Protobuf definition. This reduces the manual effort and minimizes the risk of errors when integrating services.
Strong Typing: Protobuf enforces strong typing for message structures, ensuring the consistency and integrity of data exchanged between services.
Unary and Streaming Calls: gRPC supports unary calls (single request and response) as well as streaming calls (continuous stream of requests or responses). This flexibility caters to various communication scenarios.
Practical Example: Building a Greet Service in Go
Let's create a simple gRPC server and client to demonstrate the power of gRPC using Go. They will show an example of a single request and response. As well as an example of a continuous stream from the server to the client.
Install Protobuf Compiler
First, we need to install the protobuf compiler. We must ensure that the version is 3+.
sudo apt install -y protobuf-compiler
Then, we have to install the Go plugins for the protobuf compiler.
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Define the Protobuf Messages
Create a file named idl/service/service.proto and define the Greeter service and message structure using Protocol Buffers.
syntax = "proto3";
option go_package = "internal/service";
package service;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Subscription to receive a continuous stream of notifications
rpc Subscribe (Subscription) returns (stream Notification) {}
}
// Messages definition
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
message Subscription {
string address = 1;
}
message Notification {
string message = 1;
}
Generate Go Code from Protobuf
Run the protobuf compiler to compile the idl/service/service.proto file and generate the internal/service/service.pb.go and the internal/service/service_grpc.pb.go files containing the Go implementation.
protoc --proto_path=idl --go_out=internal --go_opt=paths=source_relative --go-grpc_out=internal --go-grpc_opt=paths=source_relative service/service.proto
Implement the server in Go
Create a Go file named cmd/server/main.go and implement the server.
package main
import (
"context"
"flag"
"log"
"net"
"time"
"google.golang.org/grpc"
"go-grpc/internal/service"
)
type server struct {
service.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *service.HelloRequest) (*service.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &service.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func (s *server) Subscribe(c *service.Subscription, stream service.Greeter_SubscribeServer) error {
for {
stream.Send(&service.Notification{Message: "notification"})
time.Sleep(time.Second * 3)
}
return nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
service.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Now that we have the server implementation ready, we can compile it.
go build -o bin/server cmd/server/main.go
Implement the client in Go
Create a Go file named cmd/client/main.go and implement the client.
package main
import (
"bufio"
"context"
"io"
"log"
"os"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"go-grpc/internal/service"
)
func sayHello(c service.GreeterClient, name string) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &service.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
func subscribe(c service.GreeterClient) {
ctx := context.Background()
stream, err := c.Subscribe(ctx, &service.Subscription{Address: "client"})
if err != nil {
log.Fatalf("could not subscribe: %v", err)
}
for {
notification, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.Subscribe(_) = _, %v", c, err)
}
log.Println(notification)
}
}
func main() {
conn, err := grpc.Dial(":50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := service.NewGreeterClient(conn)
go subscribe(c)
reader := bufio.NewReader(os.Stdin)
for {
text, _ := reader.ReadString('\n')
sayHello(c, text)
}
}
We now compile the client to generate the corresponding executable.
go build -o bin/client cmd/client/main.go
Run the example
Once we have the server and client executables ready, we can run them to test the example. We start the server in one terminal.
>./bin/server
./bin/server
2023/08/01 11:07:51 server listening at [::]:50051
And the client in another one. We'll start seeing messages coming up on the client terminal. And if we type a string on the client terminal, we will see the reply from the server.
>./bin/client
./bin/client
2023/08/01 11:08:01 message:"notification"
2023/08/01 11:08:04 message:"notification"
2023/08/01 11:08:07 message:"notification"
John
2023/08/01 11:08:08 Greeting: Hello John
2023/08/01 11:08:10 message:"notification"
We can also check on the server terminal that the message from the client was received.
>./bin/server
./bin/server
2023/08/01 11:07:51 server listening at [::]:50051
2023/08/01 11:08:08 Received: John
Conclusion
gRPC is a game-changer in the world of distributed systems, providing a fast, efficient, and flexible communication framework. In this article, we explored the key features and advantages of gRPC and demonstrated a practical example using Go. As you venture into building distributed systems, consider integrating gRPC to experience the full potential of this cutting-edge technology.