Part-1: Building a basic microservice with gRPC using Golang

Introduction

Objective

What is gRPC?

Why use gRPC?

Where to use gRPC?

How to use gRPC?

Prerequisites

Go

gRPC

~/disk/E/workspace/grpc-eg-go
$ go get -u google.golang.org/grpc

Protocol Buffers v3

~/disk/E/workspace/grpc-eg-go
$ go get -u github.com/golang/protobuf/proto
~/disk/E/workspace/grpc-eg-go
$ go get -u github.com/golang/protobuf/protoc-gen-go

Setting Project Structure

~/disk/E/workspace/grpc-eg-go
$ go mod init github.com/toransahu/grpc-eg-go
$ mkdir machine
$ mkdir server
$ mkdir client
$ tree
.
├── client/
├── go.mod
├── machine/
└── server/

Defining the service

service Machine {
...
}
// Execute accepts a set of Instructions from the client and returns a Result.
rpc Execute(InstructionSet) returns (Result) {}
// Result represents the output of execution of the instruction(s).
message Result {
float output = 1;
}

Generating client and server code

~/disk/E/workspace/grpc-eg-go
$ SRC_DIR=./
$ DST_DIR=$SRC_DIR
$ protoc \
-I=$SRC_DIR \
--go_out=plugins=grpc:$DST_DIR \
$SRC_DIR/machine/machine.proto
~/disk/E/workspace/grpc-eg-go
$ tree machine/
.
├── machine/
│ ├── machine.pb.go
│ └── machine.proto

Server

type MachineServer struct{}// Execute runs the set of instructions given.
func (s *MachineServer) Execute(ctx context.Context, instructions *machine.InstructionSet) (*machine.Result, error) {
return nil, status.Error(codes.Unimplemented, "Execute() not implemented yet")
}

Implementing Simple RPC

type Stack []float32func (s *Stack) IsEmpty() bool {
return len(*s) == 0
}
func (s *Stack) Push(input float32) {
*s = append(*s, input)
}
func (s *Stack) Pop() (float32, bool) {
if s.IsEmpty() {
return -1.0, false
}
item := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return item, true
}
type OperatorType stringconst (
PUSH OperatorType = "PUSH"
POP = "POP"
ADD = "ADD"
SUB = "SUB"
MUL = "MUL"
DIV = "DIV"
)
type MachineServer struct{}// Execute runs the set of instructions given.
func (s *MachineServer) Execute(ctx context.Context, instructions *machine.InstructionSet) (*machine.Result, error) {
if len(instructions.GetInstructions()) == 0 {
return nil, status.Error(codes.InvalidArgument, "No valid instructions received")
}
var stack stack.Stack for _, instruction := range instructions.GetInstructions() {
operand := instruction.GetOperand()
operator := instruction.GetOperator()
op_type := OperatorType(operator)
fmt.Printf("Operand: %v, Operator: %v", operand, operator) switch op_type {
case PUSH:
stack.Push(float32(operand))
case POP:
stack.Pop()
case ADD, SUB, MUL, DIV:
item2, popped := stack.Pop()
item1, popped := stack.Pop()
if !popped {
return &machine.Result{}, status.Error(codes.Aborted, "Invalide sets of instructions. Execution aborted")
}
if op_type == ADD {
stack.Push(item1 + item2)
} else if op_type == SUB {
stack.Push(item1 - item2)
} else if op_type == MUL {
stack.Push(item1 * item2)
} else if op_type == DIV {
stack.Push(item1 / item2)
}
default:
return nil, status.Errorf(codes.Unimplemented, "Operation '%s' not implemented yet", operator)
}
} item, popped := stack.Pop()
if !popped {
return &machine.Result{}, status.Error(codes.Aborted, "Invalide sets of instructions. Execution aborted")
}
return &machine.Result{Output: item}, nil
}

Code to run the gRPC server

var (
port = flag.Int("port", 9111, "Port on which gRPC server should listen TCP conn.")
)
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
machine.RegisterMachineServer(grpcServer, &server.MachineServer{})
grpcServer.Serve(lis)
log.Printf("Initializing gRPC server on port %d", *port)
}

Client

var (
serverAddr = flag.String("server_addr", "localhost:9111", "The server address in the format of host:port")
)
func runExecute(client machine.MachineClient, instructions *machine.InstructionSet) {
log.Printf("Executing %v", instructions)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := client.Execute(ctx, instructions)
if err != nil {
log.Fatalf("%v.Execute(_) = _, %v: ", client, err)
}
log.Println(result)
}
func main() {
flag.Parse()
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithBlock())
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
client := machine.NewMachineClient(conn)
// try Execute()
instructions := []*machine.Instruction{}
instructions = append(instructions, &machine.Instruction{Operand: 5, Operator: "PUSH"})
instructions = append(instructions, &machine.Instruction{Operand: 6, Operator: "PUSH"})
instructions = append(instructions, &machine.Instruction{Operator: "MUL"})
runExecute(client, &machine.InstructionSet{Instructions: instructions})
}

Test

Server

~/disk/E/workspace/grpc-eg-go
$ go test server/machine.go server/machine_test.go -v
=== RUN TestExecute
--- PASS: TestExecute (0.00s)
PASS
ok command-line-arguments 0.004s

Client

~/disk/E/workspace/grpc-eg-go
$ go get github.com/golang/mock/mockgen@latest
~/disk/E/workspace/grpc-eg-go
$ mkdir mock_machine && cd mock_machine
$ mockgen github.com/toransahu/grpc-eg-go/machine MachineClient > machine_mock.go
~/disk/E/workspace/grpc-eg-go
$ go test mock_machine/machine_mock.go mock_machine/machine_mock_test.go -v
=== RUN TestExecute
output:30
--- PASS: TestExecute (0.00s)
PASS
ok command-line-arguments 0.004s

Run

Server

~/disk/E/workspace/grpc-eg-go
$ go run cmd/run_machine_server.go

Client

~/disk/E/workspace/grpc-eg-go
$ go run client/machine.go
Executing instructions:<operator:"PUSH" operand:5 > instructions:<operator:"PUSH" operand:6 > instructions:<operator:"MUL" >
output:30

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store