Using gRPC with Go
17.12.2020
There are plenty of excellent docs, articles and tutorials available regarding gRPC, thus this blog article aims to provide a rather practical guide to gRPC for people who likes to code in Go.
What is gRPC
gRPC stands for google remote procedure call and proved being a fast, scalable and stable RPC framework that runs in any environment.
At the time of this writing it supports 11 rather famous programming languages, so that services written in any of them can communicate with each other without any custom data mapping or adapters.
Whereas REST only supports the traditional single request/response paradigm, gRPC additionally supports one-directional and even bi-directional streaming, which can be used to batch multiple requests or inverse the communication direction. One-directional streaming can be applied either to the client or to the server. The former can be used to send more than one request to a single server within one session. The latter may be used to let a single server inform the requesting client about arbitrary events happening at some point in the future, which effectively solves the long-polling approach of clients in a REST-like scenario.
Anyways, in both cases the connection keeps open until the streaming side closes it.
If both sides uses streams (bi-directional streaming) gRPC effectively functions like WebSockets. This is especially useful for games, where the player continuously sends positional and action data to a dedicated server, which constantly informs all players about any world update in real-time.
It is worth to mention that the client and server can write to their streams at any time independently of each other.
In short, gRPC provides both, stateless (single request/response) as well as stateful (streaming) communication across several languages and platforms.
How to define gRPC services
gRPC services are defined in proto definition files *.proto
written in the IDL Protobuf.
The overall structure is quite simple, since there exist only the two entities message
and service
.
The following example defines a basic REST-like gRPC request/response service named Echo
:
// pkg/v1/echo/echo.proto
syntax = "proto3";
package echo;
option go_package = "v1/echo";
service Echo {
rpc Echo (EchoRequest) returns (EchoResponse) {}
}
message EchoRequest {
string text = 1;
}
message EchoResponse {
string echo = 1;
}
The fields of messages must be indexed starting from 1
. It is not allowed to define the same index twice or reuse the index of a field that has been removed between two versions. This ensures backwards compatibility when adding or removing fields in subsequent versions.
Among others the following data types are supported:
string
, int32
, int64
, float
, double
, bool
and bytes
.
We can also define enumerations:
message Message {
enum Result {
option allow_alias = true;
SUCCEEDED = 0;
FAILED = 1;
BROKEN = 1;
}
Result result = 1;
}
Note that enumeration indexes start from 0
and must be in order and not be used twice, except the option allow_alias
is set to true
(default false
), which allows to specify aliases for a given index. Here FAILED
and BROKEN
mean the same thing since they are both indexed with 1
.
Arrays or slices can be defined using the repeated
keyword, i.e.
message Message {
repeated string myArray = 1;
}
There is much more to proto files like maps, nested types and default values, all of which can be found in more detail here.
How to compile gRPC services
In order to implement this service we first need to compile echo.proto
into our favourite programming language using protoc. Since we from x-cellent are passionated Go programmers, the following command that additionally uses protoc-gen-go and protoc-gen-go-grpc produces two echo stubs written in Go (pkg/v1/echo/echo.pb.go
and pkg/v1/echo/echo_grpc.pb.go
):
protoc -I pkg \
--go_out=pkg --go_opt=paths=source_relative \
--go-grpc_out=require_unimplemented_servers=true:pkg --go-grpc_opt=paths=source_relative \
pkg/v1/echo/echo.proto
These stubs contain everything needed to implement our echo service.
Simple echo implementation
Our client could look like this:
// client/client.go
package client
import (
"context"
"google.golang.org/grpc"
"pkg/v1/echo"
)
// Echo sends the given text to the gRPC echo server and returns its echo response.
func Echo(text string) (string, error) {
// dial into the local gRPC server at TCP port 50005
conn, err := grpc.Dial(":50005", grpc.WithBlock(), grpc.WithInsecure())
if err != nil {
return "", err
}
defer conn.Close()
// create a new gRPC echo client through the compiled stub
client := echo.NewEchoClient(conn)
// send an echo request
resp, err := client.Echo(context.Background(), &echo.EchoRequest{Text: text})
if err != nil {
return "", err
}
return resp.Echo, nil
}
The server could look like this:
// server/server.go
package server
import (
"context"
"google.golang.org/grpc"
"log"
"pkg/v1/echo"
)
// echoService is the implementation of the gRPC echo service.
type echoService struct {
// enables forward compatible implementations;
// can be removed if we use '--go-grpc_out=require_unimplemented_servers=false' for protoc
echo.UnimplementedEchoServer
}
func (*echoService) Echo(ctx context.Context, req *echo.EchoRequest) (*echo.EchoResponse, error) {
// as we implement an echo server we simply return the text from the request
return &echo.EchoResponse{
Echo: req.Text,
}, nil
}
// Run starts a local echo gRPC server at TCP port 50005.
func Run() {
// create a new gRPC server that listens on local TCP port 50005
s := grpc.NewServer()
listener, err := net.Listen("tcp", ":50005")
if err != nil {
return err
}
// register our echo service through the compiled stub
echo.RegisterEchoServer(s, &echoService{})
// start the gRPC server
return s.Serve(listener)
}
Finally we need a main method to start our echo application:
// main.go
package main
import (
"client"
"log"
"server"
"time"
)
func main() {
// start the gRPC server in background
go func() {
log.Fatal(server.Run())
}
// sleep some time to let the server startup
time.Sleep(200 * time.Millisecond)
// run a client request
echo, err := client.Echo("Echo")
if err != nil {
panic(err)
}
// print the result, which should be "Echo"
println(echo)
}
Run
go run main.go
Demo project
A slightly more sophisticated gRPC demo project than the echo server that also uses gRPC streams can be found here.
Happy coding!