dev.ms

Golang, gRPC & Protobuf stories

gRPC API: Easy start

Posted at — Nov 10, 2019

In this article I’m going to describe how to build simplest gRPC API. Building such API is consists of several steps:

  1. First of all, we have to define our API interface with protocol buffers or protobuf.
  2. Then we have to generate Golang server and client stubs with protoc command.
  3. And finally we need to implement our API.

What our service will be doing is a calculations. At the moment it has just one method Add which takes A and B and returns C as an addition result.

Project can be found on github.

Protocol buffers or protobuf

Here is calc.proto file with protobuf definition of the service:

syntax = "proto3";
option go_package = "pb";

message Request {
  int32 a = 1;
  int32 b = 2;
}

message Response {
  int64 c = 1;
}

service Calc {
  rpc Add (Request) returns (Response);
}

This definition contains Calc service with one Add method which takes message Request with a and b fields and returns Response with c field. Numbers in message definitions are called tags and used by gRPC server and client in encoding/decoding messages procedures. In fact, it’s a field position inside decoded message.

This definition is language independent, that means using this file we can generate server and client stubs for any language supported by protobuf compiler protoc.

protoc supports number of languages out-of-the-box, but for Golang we have to use plugin. In our case we need plugin for Golang called protoc-gen-go. Plugins are just a binaries which should be located in any directory available in $PATH. Plugin name has a format protoc-gen-<suffix>, where:

Server and client stubs

Ok, let’s generate it:

protoc --go_out=plugins=grpc:. ./calc.proto

Here:

The command creates calc.pb.go file in the same directory with server and client interfaces, structures for request and response and some other functions for encoding/decoding messages.

Most interesting parts for us from the new file are Request and Response structures and CalcServer/CalcClient interfaces, you can see below:

type Request struct {
	A int32 `protobuf:"varint,1,opt,name=a,proto3" json:"a,omitempty"`
	B int32 `protobuf:"varint,2,opt,name=b,proto3" json:"b,omitempty"`
}

type Response struct {
	C int64 `protobuf:"varint,3,opt,name=c,proto3" json:"c,omitempty"`
}

type CalcServer interface {
	Add(context.Context, *Request) (*Response, error)
}

type CalcClient interface {
	Add(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}

gRPC server implementation

We have protobuf service definition and auto-generated code by this definition. Now, we will be implementing gRPC server to be able to accept new connections and return responses.

type server struct{}

func (s *server) Add(ctx context.Context, req *pb.Request) (*pb.Response, error) {
	fmt.Printf("got request: A = %d, B = %d\n", req.A, req.B)
	return &pb.Response{
		C: req.A + req.B,
	}, nil
}

func main() {
	s := grpc.NewServer()
	pb.RegisterCalcServer(s, &server{})

	lis, err := net.Listen("tcp", ":5001")
	if err != nil {
		return
	}

	s.Serve(lis)
}

server structure implements CalcServer interface from calc.pb.go file, which represents gRPC server methods. In main function we:

Build and start the server:

% go build && ./server

Now we need a client and here we are:

func main() {
	cc, err := grpc.Dial("127.0.0.1:5001", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer cc.Close()

	client := pb.NewCalcClient(cc)
	resp, err := client.Add(context.Background(), &pb.Request{A: 2, B: 2})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("response is: %d\n", resp.C)
}

In the client code we:

Now, let’s build and run the client in the separate terminal session:

% go build && ./client
response is: 4

while in server terminal we’ll see request info like this:

% ./server
got request: A = 2, B = 2

That’s pretty enough to the simplest gRPC API.