A Basic gRPC Example

Background

Many infrastructure developers have experienced this tedium: you have some functionality on one host you would like to access on another. For a while now, language runtimes have shipped with some sort of remote access mechanism. For Go, the gob encoding library used in conjunction with net/rpc as a transport mechanism was a suggested solution. By time Go arrived on the scene, most companies were operating heterogenous environments so limiting distributed functionality simply to Go wasn't realistic.

Dealing with heterogenous environments requires the adoption of WAN-friendly technologies that can be adapted to the LAN with a little bit of effort. Traditionally, this meant the developer would:

Large companies like Google and Facebook had long wrestled with the first challenge - defining a WAN-friendly message format. Protocol buffers was created to meet this need. From there, it became obvious that it should be possible to automatically generate both servers and clients to bind messages to local function calls directly. This is the motivation for gRPC.

Why gRPC Is Worth Considering

Building complex APIs with web technologies is achievable but painful, and gRPC addresses many of the pain points developers hit again and again:

HTTP APIs are still necessary to establish browser communication (there are attempts at getting browsers to speak directly to gRPC services but these seem quite hacky). At this point my personal approach is to create the thinnest HTTP layer possible with gRPC services doing most of the interesting work behind the scenes.

A Simple Example: Counting

I'll illustrate how gRPC works with Go by creating a very simple sample project - a server that offers two functions:

Prerequisites

Step One: Define Messages

We've already defined the spec for our API above. The first step is to build a protocol buffers definition of the input, output and rpc types that gRPC will use to generate stub code.

counter.proto

For this example, we only care about Go and will only generate stub definitions for Go, but keep in mind that the primary value of gRPC is that it will generate native messages and service bindings in any of the supported languages.

If you have followed the instructions in the prerequisites, you should be able to generate the Go stub by typing: make protoc in the counter directory.

The resulting file counter/counter.pb.go should be generated and look like this. Even though this file looks a bit messy, review it. Note how there are native Go type definitions for your protocol buffer message types, along with getters to extract values. I recommend making as little use of these generated types as possible. The first thing your rpc endpoints should do is to extract values using the getters and put them into instances of types that you control and test.

Also note the use of Go interfaces in the generated code, namely CounterClient and CounterServer. Ultimately, it is up to you to decide how the internal implementations of these endpoints work. All Go requires is that you satisfy the interface. This is a nice feature of Go that makes working with gRPC more straightforward than in other languages that don't have the notion of compile-time interface satisfaction. Note that the generated functions contain an error return parameter that is idiomatic for Go. Make use of this for application errors and try to avoid embedding custom error values into your protocol buffer messages. The functions you will write that satisfy the gRPC interfaces should be as shallow as possible. I recommend putting domain-specific functionality into private methods that you can test independently of using gRPC. This is done in a contrived manner in the example code to illustrate the point.

Step Two: Write The Server

The CounterServer interface demands two functions be implemented:

Note that as mentioned above, the satisfying implementations only extract embedded values from generated types to call a private function. Note that there is also a generated function RegisterCounterServer that is used to bind our implementation to the gRPC generated server. This is where the CounterServer interface must be satisfied by the server instance s.

Compile this with go build and start it running.

counter_server.go

Step Three: Write The Client

The CounterClient interface offers a calling convention analogous to the methods in CounterServer defines.

Build this with go build, run like:

counter_client.go

Summary

I hope this was a useful introduction to using gRPC with Go. Generating servers and clients can save you a lot of boilerplate work as long as you treat the resulting generated functions and types as only a thin layer to your own application code. Don't be afraid to read the generated code - this is key to understanding the proper use of gRPC.

last update 2019-09-17