Unary gRPC with Golang

GRPC Go (1)

This is Part 2 of the gRPC tutorials. If you haven’t checked that out, here’s the link: Introduction to gRPC with Golang. Since we covered all the basics, we’ll directly jump into implementing a unary gRPC with GO.

  • Define a proto service
  • Implement server in Go
  • Implement client in Go
  • Error Handling

Homebrew and Protoc Installation

Install Homebrew from the instructions on the original website:

Then we install protoc using the command:

brew install protobuf

Next, we need the grpc and protoc libraries for Go:

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

Implementing the gRPC

So let’s start with the implementation. First, we create a folder to store all our proto files and then create a new file mem_message.proto inside it. This will interact with memory:

We need to specify the syntax first as proto3. If we don’t do this, the protocol buffer compiler will assume you are using proto2:

syntax = "proto3";

Then we create units of memory which we can then assign to other functions:

message Memory {
  enum Unit {
    UNKNOWN = 0;
    BIT = 1;
    BYTE = 2;
    KILOBYTE = 3;
    MEGABYTE = 4;
    GIGABYTE = 5;
    TERABYTE = 6;
  }

  uint64 value = 1;
  Unit unit = 2;
}

Similarly, we create a processor message using the memory:

import "memory_message.proto";

message CPU {
  string brand = 1;
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  Memory memory = 5;
}

A storage:

import "memory_message.proto";

message Storage {
  enum Driver {
    UNKNOWN = 0;
    HDD = 1;
    SSD = 2;
  }

  Driver driver = 1;
  Memory memory = 2;
}

Similarly, we’ll create a few more for filtering and authentication. You can find all of them here: https://github.com/arkaprabha-majumdar/grpc-protoc/tree/main

Then now we see the magic of proto3. Go to terminal and a separate folder is created, with the corresponding generated Go code ! This is because of the following command we added:

option go_package = "gengo";

As you can see in each message:

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
// protoc v3.10.0
// source: store_message.proto

Next, we implement the handler. So we create a new folder, and create server.go:

// myServer is the server that provides laptop services
type myServer struct {
}

// NewServer returns a new myServer
func NewServer () *myServer{
    return &myServer{}
}

Next, we implement a create function for our laptop that the server needs:

// unary RPC to create new laptop
func (server *myServer) CreateLaptop(
    ctx context.Context,
    req *pb.CreateLaptopRequest,
) (*pb.CreateLaptopResponse, error) {
    laptop := req.GetLaptop()
    log.Printf("receive a create-laptop request with id: %s", laptop.Id)
}

You’ll notice we use the same function names as those generated by the proto-gen-go.

Next we install the Google UUID package using:

go get github.com/google/uuid

So now we can parse the laptop ID. If it doesn’t have one, we try to generate a new laptop ID. Otherwise we throw an error.

if len(laptop.Id) > 0 {
    // check if it's a valid UUID
    _, err := uuid.Parse(laptop.Id)
    if err != nil {
        return nil, status.Errorf(codes.InvalidArgument, "laptop ID invalid: %v", err)
    }
} else {
    id, err := uuid.NewRandom()
    if err != nil {
        return nil, status.Errorf(codes.Internal, "cannot generate new ID: %v", err)
    }
    laptop.Id = id.String()
}

Next, we check if the request is a timeout or canceled by the client or not, to reduce unnecessary bandwidth loss.

To check this, we simply use the ctx.Err() function:

f ctx.Err() == context.Canceled {
    log.Print("request is canceled")
    return nil, status.Error(codes.Canceled, "request already canceled")
}

if ctx.Err() == context.DeadlineExceeded {
    log.Print("deadline exceeded")
    return nil, status.Error(codes.DeadlineExceeded, "deadline exceeded")
}

Then to store the laptop that we are receiving, we create a new file store.go :

// LaptopStore ...
type LaptopStore interface {
    // Save saves the laptop to the store
    Save(laptop *pb.Laptop) error
}
// InMemoryLaptopStore ...
type InMemoryLaptopStore struct {
    mutex sync.RWMutex
    data  map[string]*pb.Laptop
}
// NewInMemoryLaptopStore ...
func NewInMemoryLaptopStore() *InMemoryLaptopStore {
    return &InMemoryLaptopStore{
        data: make(map[string]*pb.Laptop),
    }
}

This will store the laptop in-memory. We could also alternatively create a database. We use a read-write mutex to handle concurrent requests.

We have used the Save function, and still didn’t define it. So we’ll do that now:

// Save ...
func (store *InMemoryLaptopStore) Save(laptop *pb.Laptop) error {
    store.mutex.Lock()
    defer store.mutex.Unlock()

    if store.data[laptop.Id] != nil {
        return ErrAlreadyExists
    }

    other, err := deepCopy(laptop)
    if err != nil {
        return err
    }

    store.data[other.Id] = other
    return nil
}

That will be about it for the server. We’ll test the server.

Test the handler

Let’s create a server_test.go file, and add the test cases in a table. Check https://golangdocs.com/golang-unit-testing for more details.

func TestServerCreateLaptop(t *testing.T) {
    t.Parallel()

    testCases := []struct {
        name   string
        laptop *pb.Laptop
        store  service.LaptopStore
        code   codes.Code
    }{
        {
            name:   "success_with_id",
            laptop: sample.NewLaptop(),
            store:  service.NewInMemoryLaptopStore(),
            code:   codes.OK,
        },
        {
            name:   "success_no_id",
            laptop: laptopNoID,
            store:  service.NewInMemoryLaptopStore(),
            code:   codes.OK,
        },
        {
            name:   "failure_invalid_id",
            laptop: laptopInvalidID,
            store:  service.NewInMemoryLaptopStore(),
            code:   codes.InvalidArgument,
        },
        {
            name:   "failure_duplicate_id",
            laptop: laptopDuplicateID,
            store:  storeDuplicateID,
            code:   codes.AlreadyExists,
        },
    }
}

You can add more tests that you feel like.

Now that we’re done we need to connect the gRPC to our real server, and then implement the actual server and client. So hop onto the next part for the final segment: Unary gRPC with Golang2

References