Scaffolding A gRPC service with Go (Golang) For Production

by Sabbir Ahmed


Posted on: 2 years, 1 month ago


gRPC with GoLang (entgo.io)
gRPC with GoLang (entgo.io)

gRPC is google's high-performance RPC framework. Working with Remote Procedural Calls (RPC) has always been a headache before gRPC. Google has made it easier for developers and engineers with polyglot code generation support. Today we are going to have a look at the modern defacto standard for microservices and service-to-service communication protocol- gRPC.

This article is not going to cover the installation of the protobuf and go packages. They are pretty straightforward and you can find them easily at - Go Quick Start.

Getting Started

In this article, we will create a Basic Geometry service that will return us the geometric property of a shape ie, circle, rectangle, etc... So, Let's begin. I am a big fan of project structures. It helps me to understand what I am going to do and where I have to look for when it comes to resolving any bug/issue. So, I am going to start with our project's folder structure -

.
├── cmd
│   ├── client
│   └── server
├── internal
├── proto
└── go.mod

 

Get the source code and follow along...

Code Coverage, Unit Tests and Code Quality- Golang, Go, sabbir.dev

The Proto File

Almost every go project will have a cmd folder. It may container cli, server, client according to the project requirement. Internal will contain the factories, repositories, and services. Now we are ready to define the contract between server and client aka the proto. Create a new file at proto directory named geometry.proto and paste the following - 

syntax = "proto3";
package geometry;

option go_package = "github.com/by-sabbir/grpc-service-example/proto";

These three lines of code are the setups. The first line denotes the version we are using to define the proto. The second line is the package name, we can be creative here. The 3rd line is the go-specific option, this is an amalgamation of the module name from the go.mod and the relative path of the proto directory.

Defining Services in Proto

Now, we are ready to declare the service and Request/Response contract - gRPC ensures the communication by setting up a pre-defined message structure so the server and client don't confuse themselves with the message structure. This may sound unnecessary for beginners but in my software development experience, this was one of the most frustrating points. And gRPC addressed this issue right at the beginning. An RPC service has three parts,

  1. The service
  2. Request message
  3. Response message

in Proto point 2 and 3 is part of the message struct and service is the service struct, dead simple. Let's code - extend the geometry.proto file and add the following lines - 

message RectRequest {
    float height = 1;
    float width = 2;
}

message AreaResponse {
    float result = 1;
}

message PermiterResponse {
    float result = 1;
}

service GeometryService {
    rpc Area (RectRequest) returns (AreaResponse);
    rpc Perimeter (RectRequest) returns (PermiterResponse);
}

The proto itself is self-descriptive, we have a GeometryService with two functions - Area and Perimeter. Both RPC functions take a request object and spit out a response. Generally, the request and response messages are named with the same prefix, for example, if we had a service called Greet, we would have had a GreetRequest and a GreetResponse (Greet as prefix). But our service is a perfect example of making an exception.

Generating Code with Protoc

In the beginning, we were bragging about the gRPC being polyglot, in this section, we will see it in action. We will generate some go code with protoc. Note that we must need the go plugins for it, check if you have already installed them at Go Quick Start.

The basic command is as follows - 

 protoc --go_out=. --go-grpc_out=. proto/*.proto

This should generate the go interface codes as follows.

├── github.com
    └── by-sabbir
        └── grpc-service-example
            └── proto
                ├── geometry_grpc.pb.go
                └── geometry.pb.go

Oh! now we see the patterns right? This is the folder structure we assigned go_package in the third line of the geometry.proto file... the resemblance is uncanny (pun intended). But we don't need them here, we need them in the proto directory. If you are thinking of changing the go_package, you are not wrong. But there is a better approach. The protoc CLI tool allows us to assign modules and import paths for the generated code. let's update the command

 protoc -Iproto/ --go_out=. --go_opt=module=github.com/by-sabbir/grpc-service-example --go-grpc_out=. --go-grpc_opt=module=github.com/by-sabbir/grpc-service-example proto/*.proto

If I am not wrong you must be thinking who can remember the huge command! let me demystify that for you. But in practice, you will always use Makefile or equivalent to generate the codes for you. So the concise command should look like this

 protoc -I<proto path> --go_out=. --go_opt=module=<go module> --go-grpc_out=. --go-grpc_opt=module=<go module> proto/*.proto

For every _out, we will have _opt=module= and the module name from the go.mod file. We can delete the github.com folder as we have the generated code in the proto directory.

Implementing the gRPC Server

We have come a long way, let's create a new file in the cmd/server directory and name it main.go unsurprising stuff. And run the following command to get the dependencies-

 go mod tidy

Let's first copy the following code in the cmd/server/main.go from GitHub And the cmd/client/main.go from GitHub.

At this point, we are ready to test our applications,

 go run cmd/server/main.go

The output should be like below - 

2022/09/26 16:18:17 tcp listener started at port:  5000
2022/09/26 16:18:21 invoked Area:  height:10.1  width:20.5
2022/09/26 16:18:21 invoked Perimeter:  height:10.1  width:20.5

If the code exists with status code 1, you should change the port at line 16 of the server/main.go, else run the client:

 go run cmd/client/main.go

If the output shows like below we are good to go to the next step

Area:  207.05
Perimeter:  61.2

Test with some values in the client code.

Refactoring The Server For Production

In go, it's common practice to start with a single file and then refactor to best practices. Currently, we have a monolithic server with no separation of concern. We started the project with growth factors in mind, we had an untouched folder named internal. Let's create a folder geometry and a file geometry.go, so the path looks like internal/geometry/geometry.go. This will be our Factory for the Geometry Service. Let's have a look at the geometry.go file 

package geometry

import (
        "context"
        "log"

        pb "github.com/by-sabbir/grpc-service-example/proto"
)

type Store interface {
        Area(context.Context, *pb.RectRequest) (*pb.AreaResponse, error)
        Perimeter(context.Context, *pb.RectRequest) (*pb.PermiterResponse, error)
}

type Server struct {
        pb.GeometryServiceServer
}

func NewServer() *Server {
        return &Server{}
}

func (s *Server) Area(ctx context.Context, in *pb.RectRequest) (*pb.AreaResponse, error) {
        log.Println("invoked Area: ", in)
        return &pb.AreaResponse{
                Result: in.Height * in.Width,
        }, nil
}

func (s *Server) Perimeter(ctx context.Context, in *pb.RectRequest) (*pb.PermiterResponse, error) {
        log.Println("invoked Perimeter: ", in)
        return &pb.PermiterResponse{
                Result: 2 * (in.Height + in.Width),
        }, nil
}

The Store interface will act as a Repository. if we need to expand the project and integrate it with a database we can create a new folder db in the internal directory and just implement the Store interface. And now our cmd/server/main.go file should look like the following,

package main

import (
        "fmt"
        "log"
        "net"
        "os"

        geomServer "github.com/by-sabbir/grpc-service-example/internal/geometry"
        pb "github.com/by-sabbir/grpc-service-example/proto"
        "google.golang.org/grpc"
)

var (
        host = "localhost"
        port = "5000"
)

func main() {
        addr := fmt.Sprintf("%s:%s", host, port)
        lis, err := net.Listen("tcp", addr)

        if err != nil {
                log.Println("error starting tcp listener: ", err)
                os.Exit(1)
        }
        log.Println("tcp listener started at port: ", port)
        grpcServer := grpc.NewServer()
        geomServiceServer := geomServer.NewServer()
        // registering gemoetry service server into grpc server
        pb.RegisterGeometryServiceServer(grpcServer, geomServiceServer)

        if err := grpcServer.Serve(lis); err != nil {
                log.Println("error serving grpc: ", err)
                os.Exit(1)
        }
}

As we can see now, the server is not burdened with the Area and Perimeter functions anymore. It's just a simple gRPC server implementation, concerns are separated.

Improvements

  • Maintaining a separate repository for the proto directory
  • Version the proto files ie, proto/v1

Note: If you have any questions regarding this article please follow up on LinkedIn or Telegram. I will try my best to answer your queries.