Sudo Rambles
  • Home
  • Privacy Policy
  • About
  • Contact
Categories
  • cheat-sheets (2)
  • guides (11)
  • news (1)
  • ramblings (4)
  • tutorials (10)
  • Uncategorized (9)
Sudo Rambles
Sudo Rambles
  • Home
  • Privacy Policy
  • About
  • Contact
  • guides
  • tutorials
  • Uncategorized

gRPC and Go – the simple guide

  • 16th November 2022

What is gRPC again?

All the buzz in the enterprise world is currently revolving around gRPC. Check out the proper description of what it is here: link. From my experience, you’ll resort to gRPC by default, and you may forget about REST completely.
Integrating any number of gRPC calls into a GO application is super simple, although simply reading the docs you might not give it a chance. So let’s get to it and integrate a couple of endpoints into an existing GO app.
Note that this tutorial will be showing single call declarations only. Expect a stream tutorial soon!

The preface

So I’ve been creating a test app that contains a number of endpoints revolving around CRUD. With time, as I wanted to add more and more components to it, it has grown a bit. Feel free to use it as an example, GitHub link here.
In this tutorial we’ll be using code that’s already integrated in that repo.

Protobufs

So a protobuf is a definition of a given call. Think of this as an alternative to the REST endpoint. You create the definitions in a service, which each declaration having a request and response object.
The file we need to create, that will contain all of these definitions is a called a proto file and it looks like so:

syntax = "proto3";

option go_package = "some_go_package_path";

package your_package;

service ExampleService {
  rpc GetAnExample(GetExampleRequest) returns (GetExampleResponse);
}

message GetExampleRequest {
  string id = 1;
}

message GetExampleResponse {
  Example example = 1;
}

message Example {
  string id = 1;
  string name = 2;
  string h_created_at = 6; // human readable timestamp
  string h_updated_at = 7; // human readable timestamp
}

You can see how we define both a request and a response, and we use them within a call definition.
The “Example” message definition is of a re-usable component, you might need to return the same structure in both a single get call and a listing call. Thus, defining it once and re-using it is considered good-practice.

Here’s an example of a full-blown proto definition from the aforementioned repo (link)

Proto vs. protobuf

Now that you’ve created your proto definition, you’ll want to use it within your service. To do that we need to generate a static interpretation of the file. That’s called a protobuf file. That’s what’s actually loaded into our service, and it also provides us with version control, ensuring that breaking and non-breaking changes are applied properly.

Generating the file is quite simple, we’ll be using protoc for that. The command is:

protoc --proto_path=protob "PATH_TO_PROTO_FILE"\
--go_out=GENERATED_PROTOB_PATH --go_opt=paths=source_relative \
--go-grpc_out=GENERATED_PROTOB_PATH --go-grpc_opt=paths=source_relative

Once you run this command, you’ll see two files generated in your OUT directory:

/YOURFILE_grpc.pb.go
/YOURFILE.pb.go

The first one is the static gRPC related declarations, let’s ignore that one. The one we’ll be using and focusing on is the “YOURFILE.pb.go”. That’s the file that we’ll be loading into our service, and that GO will be referring to when called.

See the full-blown examples here

Additional notes

As this is a test app, the generated protobufs will be in the same root directory as your logic. So we can just import them in our handler. In a production environment, you might want to separate those two parts into separate repos. Once you do that, each time you make changes to your proto files, you’ll need to “go get” the new version.

Using the definitions

In the REST world we use controllers, in the gRPC world the same function is covered by handlers. Essentially they are the same thing, I just wanted to introduce you to the lingo.

Let’s create a simple handler:

package handlers

import (
	pb "YOUR_PATH/GENERATED_PROTOB_PATH"
)

type ExampleHandler struct {
	*pb.UnimplementedExampleServiceServer
}

Hookay, what is this unimplemented declaration here? Simply put, that’s how we bind the handler to the gRPC service declaration. Think of it as an instruction that this handler implements everything declared within the ExampleService, and will handle all calls that arrive to the same.

Cool, so let’s add the get method into this handler:

package handlers

import (
	pb "YOUR_PATH/GENERATED_PROTOB_PATH"
)

type ExampleHandler struct {
	*pb.UnimplementedExampleServiceServer
}

func (e *ExampleHandler ) GetExample(ctx context.Context, request *pb.GetExampleRequest) (*pb.GetExampleResponse, error) {
	// ... validate ... get the example from the db etc.

	return &pb.GetExampleResponse{
		Stock: toExamplePb(example),
	}, nil
}

Oh boy, new things everywhere. So what are we doing here, exactly? I’m deliberately skipping the validation and fetching the record part, as these should be separate publications IMO.

We have one important parameter here: “request *pb.GetExampleRequest“. That’s obviously the request entry that arrived with the call. But it’s more than that. It’s the request as we defined it within the original proto files. When you’re about to fetch the record from the database, you’ll need the ID, let’s grab it like so:

if request.GetId() != nil {
  exampleId := request.GetId()
}

Or if you’re working with UUIDs here:

exampleId, err := uuid.Parse(request.GetId())
if err != nil {
  // handle the error 
}

Each property of every message within the proto definition has getters. Use them, they are safer than directly calling “example.Id“. If you try to grab a nil prop, the whole service will grind to a halt. Use the getters, and check their returns with an if.

Check out the definition here

The server

We are the client, and we want to call the server to grab our info. Let’s build that as well.

func (s *Serve) Serve() {
	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", s.port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	grpcServer := grpc.NewServer(*s.opts...)

	pb.RegisterExampleServiceServer(grpcServer, s.handler)

	grpcServer.Serve(lis)
}

So what are we doing here? We’re invoking a listen command on a given port, if we can’t listen on that address we throw an error.
We then create a new server from the gRPC package and we register our server declaration on it. Note that we provide the handler here as a parameter. This is the code block that links the declaration with the implementation, thus registering the handler as our “handler”.
And we invoke the Serve() method.

That’s all there is to it. If you have more than one service here, just register it with the respective handler and you’re good to go.

If you’re looking for a nice alternative to Postman, use BloomRPC. It’s a great tool to have for testing and debugging calls.

Again, you can see the full-blown working example in the repo here (link)

If this article didn’t have enough info here, check out the official guide here.

Happy coding!

Previous Article
  • guides

Dockerised ntopng on a Raspberry Pi 4

  • 17th December 2021
View Post
Sudo Rambles
  • LinkedIn
  • Twitter
A programmer's blog

Input your search keywords and press Enter.

GDPR notice
This website uses cookies to cache data localy. This means the site loads faster on later visits. Please, indicate if you're ok with this.Yep, that's fineNot my cup of tea Read More
Cookie Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary
Always Enabled
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Non-necessary
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.
SAVE & ACCEPT