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.
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!