Golang gRPC Microservice

 Introduction

        I was looking online for a good Golang gRPC Microservice tutorial using Go modules and the latest best practices and I could not find one. There are a few posts but they are old, do not use Go modules and have complex approaches and confusing code. Hence I have come up with this post to give you a comprehensive overview of Golang gRPC Microservice. Please feel free to use my application as a base to build your own application.

Golang

        Go or Golang, as it is called, from their website, is an open source programming language that makes it easy to build simple, reliable, and efficient software. It is gaining popularity due to the following features:
  1. Fast: Golang is a compiled language, the code written is directly translated into formats that any processor understands. 
  2. Lightweight and minimalist language: The formal Go language specification is only a few pages
  3. Memory safety: The compiler manages memory safety by introducing “Bounds Check” points in the code that guarantees safe access to the memory.
  4. Native Garbage collection
  5. Native concurrency: The Go language has Goroutines, that are functions that can run independently and simultaneously.
  6. Structural typing: Go is strongly and statically typed with no implicit conversions. It has simple type inference in assign­ments together with untyped numeric constants.
 Docker and Kubernetes are built using Go. 

gRPC

        gRPC, from their website,  is a high performance, open source universal Remote Procedure Call (RPC) framework initially developed at Google. gRPC uses HTTP/2 for transport and Protocol Buffers (Protobuf) as the interface description language. In gRPC, a client application can directly call a method on a server application on a different machine as if it were a local object, making it easier for you to create distributed applications and services.

Advantages of using gRPC are:
  1. Due to the use of Protocol Buffers and HTTP/2, it is faster when compared to REST while sending and receiving data.
  2. gRPC messages are serialized using Protobuf. Protobuf serializes very quickly on the server and client. Protobuf serialization results in small message payloads which is important in limited bandwidth scenarios like mobile apps.
  3. Performance and efficiency is much better when compared to REST. REST uses JSON as payload. Parsing JSON is very CPU intensive.
  4. Provides support for code generation.
Requirements to Run the Application:
  1. Golang
  2. Protocol Buffers
  3. Visual Studio Code or any IDE that supports working on Golang code.
  4. MongoDB Database
Golang should be installed in your machine. To setup and test if Golang is installed fine, please refer to my post on: Golang Setup.

Protocol Buffers should be setup on your machine. To setup and test if Protocol Buffers is installed fine, please refer to my post on: Protobuf Setup.

MongoDB should be setup and running in your machine. To setup, run and test if MongoDB is working fine, please refer to my post on: MongoDB Setup.

I prefer using Visual Studio Code for working on the Golang code. It can be downloaded from Microsoft Visual Studio Code Website.
To get to know more about Visual Studio Code, you can read: Getting Started on Visual Studio Code
If you prefer using any other tool for working on the Golang code, you can go ahead and use it.

Once you have the setup mentioned above ready, you would be able to write and execute Golang code and create the microservice in simple steps as below.

Step 1: Download and install Golang packages and dependencies

Download and install Golang packages and dependencies using the commands below:
  $ go get -u -v github.com/golang/protobuf/proto
  $ go get -u -v github.com/golang/protobuf/protoc-gen-go
  $ go get -u google.golang.org/grpc
  $ go get go.mongodb.org/mongo-driver/mongo
  $ go get -u github.com/spf13/cobra
  $ go get -u github.com/mitchellh/go-homedir
  $ go get -u github.com/spf13/viper

Step 2: Create the .proto file

In this post we are creating an Employee Microservice, to create, read, update and delete employees. In the .proto file we need to declare the structure of an Employee message and the services that are available in this microservice. Here is the employee.proto file:
  // protoc proto/employee.proto --go_out=plugins=grpc:.

syntax = "proto3";

package employee;

option go_package = ".;employeepb";

message Employee {
    string id = 1;
    string name = 2;
    string department = 3;
    int32 salary = 4;
}

message CreateEmployeeRequest {
    Employee employee = 1; // Employee id blank
}

message CreateEmployeeResponse {
    Employee employee = 1; // Employee id filled in
}

message GetEmployeeRequest {
    string id = 1;
}

message GetEmployeeResponse {
    Employee employee = 1;
}

message UpdateEmployeeRequest {
    Employee employee = 1;
}

message UpdateEmployeeResponse {
    Employee employee = 1;
}

message DeleteEmployeeRequest {
    string id = 1;
}

message DeleteEmployeeResponse {
    bool success = 1;
}

message GetAllEmployeesRequest {}

message GetAllEmployeesResponse {
    Employee employee = 1;
}

service EmployeeService {
    rpc CreateEmployee(CreateEmployeeRequest) returns (CreateEmployeeResponse);
    rpc GetEmployee(GetEmployeeRequest) returns (GetEmployeeResponse);
    rpc GetAllEmployees(GetAllEmployeesRequest) returns (stream GetAllEmployeesResponse);
    rpc UpdateEmployee(UpdateEmployeeRequest) returns (UpdateEmployeeResponse);
    rpc DeleteEmployee(DeleteEmployeeRequest) returns (DeleteEmployeeResponse);
}
In this file, we create the structure of an Employee message. Here is the code for this:
  message Employee {
    string id = 1;
    string name = 2;
    string department = 3;
    int32 salary = 4;
}
We also create 5 services. Each service has a request input parameter and a response output parameter. Here are the 5 services:
  service EmployeeService {
    rpc CreateEmployee(CreateEmployeeRequest) returns (CreateEmployeeResponse);
    rpc GetEmployee(GetEmployeeRequest) returns (GetEmployeeResponse);
    rpc GetAllEmployees(GetAllEmployeesRequest) returns (stream GetAllEmployeesResponse);
    rpc UpdateEmployee(UpdateEmployeeRequest) returns (UpdateEmployeeResponse);
    rpc DeleteEmployee(DeleteEmployeeRequest) returns (DeleteEmployeeResponse);
}
The rest of the code in this .proto file is for the requests and responses used in the 5 services. Most of the requests and responses will consist of the Employee message that we created above.

Step 3: Generate the Client and Server stubs

Using the .proto definitions, we need to generate the Client and Server stubs. Please ensure that you have installed Protocol Buffers successfully using the instructions mentioned in my post above. Compile the proto file to Go stubs using the command below:
  protoc proto/employee.proto --go_out=plugins=grpc:.
A new file employee.pb.go will be generated in the proto folder.

Step 4: Implement the server side code

The server side code will have:
1. main() function which creates a gRPC server, connection to MongoDB and also gives messages on the console when the server is running. There is also code here to help shutdown the server. Here is the related code:
  var db *mongo.Client
var employeedb *mongo.Collection
var mongoCtx context.Context

func main() {

	log.SetFlags(log.LstdFlags | log.Lshortfile)

	fmt.Println("Starting server on port :50051...")

	// 50051 is the default port for gRPC
	// Ideally we'd use 0.0.0.0 instead of localhost as well
	listener, err := net.Listen("tcp", ":50051")

	if err != nil {
		log.Fatalf("Unable to listen on port :50051: %v", err)
	}

	// Slice of gRPC options
	// Here we can configure things like TLS
	opts := []grpc.ServerOption{}
	// var s *grpc.Server
	s := grpc.NewServer(opts...)
	// var srv *EmployeeServiceServer
	srv := &EmployeeServiceServer{}

	employeepb.RegisterEmployeeServiceServer(s, srv)

	// Initialize MongoDB client
	fmt.Println("Connecting to MongoDB...")
	mongoCtx = context.Background()
	db, err = mongo.Connect(mongoCtx, options.Client().ApplyURI("mongodb://localhost:27017"))
	if err != nil {
		log.Fatal(err)
	}
	err = db.Ping(mongoCtx, nil)
	if err != nil {
		log.Fatalf("Could not connect to MongoDB: %v\n", err)
	} else {
		fmt.Println("Connected to MongoDB")
	}

	employeedb = db.Database("softwaredevelopercentral").Collection("employee")

	// Start the server in a child routine
	go func() {
		if err := s.Serve(listener); err != nil {
			log.Fatalf("Failed to serve: %v", err)
		}
	}()
	fmt.Println("Server succesfully started on port :50051")

	// Create a channel to receive OS signals
	c := make(chan os.Signal)

	// Relay os.Interrupt to our channel (os.Interrupt = CTRL+C)
	// Ignore other incoming signals
	signal.Notify(c, os.Interrupt)

	// Block main routine until a signal is received
	// As long as user does not press CTRL+C a message is not passed and our main routine keeps running
	// If the main routine were to shutdown, the child routine that is Serving the server would shutdown
	<-c

	fmt.Println("\nStopping the server...")
	s.Stop()
	listener.Close()
	fmt.Println("Closing MongoDB connection")
	db.Disconnect(mongoCtx)
	fmt.Println("Done.")
}
2. Next in the server side code, we need to add functions for the services we defined in the employee.proto file above. 
There is no relative import in Go. If you keep your code in a source repository somewhere, then you should use the root of that source repository as your base path.
Please observe how I have used the import:
employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"

All files in this project will use the same pattern in import and this will help us to use Go modules which I have explained in Step 6: Adding Go Module Support.

With all the related functions added, here is the full main.go file with the full server side code:
package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type EmployeeServiceServer struct {
}

func (s *EmployeeServiceServer) GetEmployee(ctx context.Context, req *employeepb.GetEmployeeRequest) (*employeepb.GetEmployeeResponse, error) {
	// Convert the string id (from proto) to mongoDB ObjectId
	oid, err := primitive.ObjectIDFromHex(req.GetId())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Could not convert to ObjectId: %v", err))
	}
	result := employeedb.FindOne(ctx, bson.M{"_id": oid})
	// Create an empty EmployeeItem to write our decode result to
	data := EmployeeItem{}
	// Decode and write to data
	if err := result.Decode(&data); err != nil {
		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find employee with Object Id %s: %v", req.GetId(), err))
	}
	// Cast to GetEmployeeResponse type
	response := &employeepb.GetEmployeeResponse{
		Employee: &employeepb.Employee{
			Id:         oid.Hex(),
			Name:       data.Name,
			Department: data.Department,
			Salary:     data.Salary,
		},
	}
	return response, nil
}

func (s *EmployeeServiceServer) CreateEmployee(ctx context.Context, req *employeepb.CreateEmployeeRequest) (*employeepb.CreateEmployeeResponse, error) {
	// Get the protobuf employee type from the protobuf request type
	// Essentially doing req.Employee to access the struct with a nil check
	employee := req.GetEmployee()
	// Now we have to convert this into a EmployeeItem type to convert into BSON
	data := EmployeeItem{
		Name:       employee.GetName(),
		Department: employee.GetDepartment(),
		Salary:     employee.GetSalary(),
	}

	// Insert the data into the database
	result, err := employeedb.InsertOne(mongoCtx, data)
	// check error
	if err != nil {
		// return internal gRPC error to be handled later
		return nil, status.Errorf(
			codes.Internal,
			fmt.Sprintf("Internal error: %v", err),
		)
	}
	// Add the ID to employee
	oid := result.InsertedID.(primitive.ObjectID)
	employee.Id = oid.Hex()
	// Return the employee in a CreateEmployeeResponse type
	return &employeepb.CreateEmployeeResponse{Employee: employee}, nil
}

func (s *EmployeeServiceServer) UpdateEmployee(ctx context.Context, req *employeepb.UpdateEmployeeRequest) (*employeepb.UpdateEmployeeResponse, error) {
	// Get the employee data from the request
	employee := req.GetEmployee()

	// Convert the Id string to a MongoDB ObjectId
	oid, err := primitive.ObjectIDFromHex(employee.GetId())
	if err != nil {
		return nil, status.Errorf(
			codes.InvalidArgument,
			fmt.Sprintf("Could not convert the supplied employee id to a MongoDB ObjectId: %v", err),
		)
	}

	// Convert the data to be updated into an unordered Bson document
	update := bson.M{
		"name":       employee.GetName(),
		"department": employee.GetDepartment(),
		"salary":     employee.GetSalary(),
	}

	// Convert the oid into an unordered bson document to search by ID
	filter := bson.M{"_id": oid}

	// Responseult is the BSON encoded result
	// To return the updated document instead of original we have to add options.
	result := employeedb.FindOneAndUpdate(ctx, filter, bson.M{"$set": update}, options.FindOneAndUpdate().SetReturnDocument(1))

	// Decode result and write it to 'decoded'
	decoded := EmployeeItem{}
	err = result.Decode(&decoded)
	if err != nil {
		return nil, status.Errorf(
			codes.NotFound,
			fmt.Sprintf("Could not find employee with supplied ID: %v", err),
		)
	}
	return &employeepb.UpdateEmployeeResponse{
		Employee: &employeepb.Employee{
			Id:         decoded.ID.Hex(),
			Name:       decoded.Name,
			Department: decoded.Department,
			Salary:     decoded.Salary,
		},
	}, nil
}

func (s *EmployeeServiceServer) DeleteEmployee(ctx context.Context, req *employeepb.DeleteEmployeeRequest) (*employeepb.DeleteEmployeeResponse, error) {
	oid, err := primitive.ObjectIDFromHex(req.GetId())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Could not convert to ObjectId: %v", err))
	}
	// DeleteOne returns DeleteResponseult which is a struct containing the amount of deleted docs (in this case only 1 always)
	// So we return a boolean instead
	_, err = employeedb.DeleteOne(ctx, bson.M{"_id": oid})
	if err != nil {
		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find/delete employee with id %s: %v", req.GetId(), err))
	}
	return &employeepb.DeleteEmployeeResponse{
		Success: true,
	}, nil
}

func (s *EmployeeServiceServer) GetAllEmployees(req *employeepb.GetAllEmployeesRequest, stream employeepb.EmployeeService_GetAllEmployeesServer) error {
	// Initiate a EmployeeItem type to write decoded data to
	data := &EmployeeItem{}
	// collection.Find returns a cursor for our (empty) query
	cursor, err := employeedb.Find(context.Background(), bson.M{})
	if err != nil {
		return status.Errorf(codes.Internal, fmt.Sprintf("Unknown internal error: %v", err))
	}
	// An expression with defer will be called at the end of the function
	defer cursor.Close(context.Background())
	// cursor.Next() returns a boolean, if false there are no more items and loop will break
	for cursor.Next(context.Background()) {
		// Decode the data at the current pointer and write it to data
		err := cursor.Decode(data)
		if err != nil {
			return status.Errorf(codes.Unavailable, fmt.Sprintf("Could not decode data: %v", err))
		}
		// If no error is found send employee over stream
		stream.Send(&employeepb.GetAllEmployeesResponse{
			Employee: &employeepb.Employee{
				Id:         data.ID.Hex(),
				Name:       data.Name,
				Salary:     data.Salary,
				Department: data.Department,
			},
		})
	}
	if err := cursor.Err(); err != nil {
		return status.Errorf(codes.Internal, fmt.Sprintf("Unkown cursor error: %v", err))
	}
	return nil
}

type EmployeeItem struct {
	ID         primitive.ObjectID `bson:"_id,omitempty"`
	Name       string             `bson:"name"`
	Salary     int32              `bson:"salary"`
	Department string             `bson:"department"`
}

var db *mongo.Client
var employeedb *mongo.Collection
var mongoCtx context.Context

func main() {

	log.SetFlags(log.LstdFlags | log.Lshortfile)

	fmt.Println("Starting server on port :50051...")

	// 50051 is the default port for gRPC
	// Ideally we'd use 0.0.0.0 instead of localhost as well
	listener, err := net.Listen("tcp", ":50051")

	if err != nil {
		log.Fatalf("Unable to listen on port :50051: %v", err)
	}

	// Slice of gRPC options
	// Here we can configure things like TLS
	opts := []grpc.ServerOption{}
	// var s *grpc.Server
	s := grpc.NewServer(opts...)
	// var srv *EmployeeServiceServer
	srv := &EmployeeServiceServer{}

	employeepb.RegisterEmployeeServiceServer(s, srv)

	// Initialize MongoDB client
	fmt.Println("Connecting to MongoDB...")
	mongoCtx = context.Background()
	db, err = mongo.Connect(mongoCtx, options.Client().ApplyURI("mongodb://localhost:27017"))
	if err != nil {
		log.Fatal(err)
	}
	err = db.Ping(mongoCtx, nil)
	if err != nil {
		log.Fatalf("Could not connect to MongoDB: %v\n", err)
	} else {
		fmt.Println("Connected to MongoDB")
	}

	employeedb = db.Database("softwaredevelopercentral").Collection("employee")

	// Start the server in a child routine
	go func() {
		if err := s.Serve(listener); err != nil {
			log.Fatalf("Failed to serve: %v", err)
		}
	}()
	fmt.Println("Server succesfully started on port :50051")

	// Create a channel to receive OS signals
	c := make(chan os.Signal)

	// Relay os.Interrupt to our channel (os.Interrupt = CTRL+C)
	// Ignore other incoming signals
	signal.Notify(c, os.Interrupt)

	// Block main routine until a signal is received
	// As long as user does not press CTRL+C a message is not passed and our main routine keeps running
	// If the main routine were to shutdown, the child routine that is Serving the server would shutdown
	<-c

	fmt.Println("\nStopping the server...")
	s.Stop()
	listener.Close()
	fmt.Println("Closing MongoDB connection")
	db.Disconnect(mongoCtx)
	fmt.Println("Done.")
}

Step 5: Implement the client side code

For client side implementation we will create a CLI application using Cobra. Cobra is used in many Go projects such as:  
  1. Kubernetes
  2. Hugo
  3. Github CLI
To read more about Cobra, visit their Github Link

Create a folder named as client.  Please check the folder structure for this microservice in my GitHub Repo. Execute the commands below in this folder:
  D:\projects\grpc-go-mongodb-cobra\client>go get github.com/spf13/cobra/cobra
  D:\projects\grpc-go-mongodb-cobra\client>cobra init --pkg-name main
This will create the file main.go and the folder cmd. A file root.go will also be created under the cmd folder. The code in main.go just calls execute on the root command, located in /cmd/root.go.

Update code in the file main.go as below, if you observe closely only import has been changed to handle the use of go modules:
  /*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import cmd "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/client/cmd"

func main() {
	cmd.Execute()
}
In root.go, we will:
  1. Initialize the client and connect to the server on localhost:50051.
  2. Instantiate the EmployeeServiceClient with our client connection to the server
Update the code in the file: root.go as below:
package cmd

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	homedir "github.com/mitchellh/go-homedir"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"google.golang.org/grpc"
)

var cfgFile string

var client employeepb.EmployeeServiceClient
var requestCtx context.Context
var requestOpts grpc.DialOption

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "employeeclient",
	Short: "a gRPC client to communicate with the EmployeeService server",
	Long: `a gRPC client to communicate with the EmployeeService server.
	You can use this client to create and read employees.`,
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize(initConfig)

	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.employeeclient.yaml)")

	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

	fmt.Println("Starting Employee Service Client")
	// Establish the context to timeout if the server does not respond
	requestCtx, _ = context.WithTimeout(context.Background(), 20*time.Second)
	// Establish the insecure grpc options (no TLS)
	requestOpts = grpc.WithInsecure()
	// Dial the server, this returns a client connection
	conn, err := grpc.Dial("localhost:50051", requestOpts)
	if err != nil {
		log.Fatalf("Unable to establish client connection to localhost:50051: %v", err)
	}

	// Instantiate the EmployeeServiceClient with our client connection to the server
	client = employeepb.NewEmployeeServiceClient(conn)
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
	if cfgFile != "" {
		// Use config file from the flag.
		viper.SetConfigFile(cfgFile)
	} else {
		// Find home directory.
		home, err := homedir.Dir()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		viper.AddConfigPath(home)
		viper.SetConfigName(".employeeclient")
	}

	viper.AutomaticEnv() // read in environment variables that match

	// If a config file is found, then read it in.
	if err := viper.ReadInConfig(); err == nil {
		fmt.Println("Using config file:", viper.ConfigFileUsed())
	}
}

Create Employee command: 

Next we will work on the Create Employee command. Execute the command below in this folder:
  D:\projects\grpc-go-mongodb-cobra\client>cobra add create
This will create a new file, /cmd/create.go with the layout similar to root.go . There is a struct of type cobra.Command and an init() function. In the init() function we need to define the flags, which are the input parameters for our cobra command. In the cobra.Command we get the data from our flags, create the employee protobuf message and call the RPC CreateEmployee. In the last line of the init() function our sub-command is bound to the root. Here is the full code for create.go:
  package cmd

import (
	"context"
	"fmt"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	"github.com/spf13/cobra"
)

var createCmd = &cobra.Command{
	Use:   "create",
	Short: "Create a new employee",
	Long: `Create a new employee on the server through gRPC. 
	
	A employee post requires an Name, Department and Salary.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		name, err := cmd.Flags().GetString("name")
		department, err := cmd.Flags().GetString("department")
		salary, err := cmd.Flags().GetInt32("salary")
		if err != nil {
			return err
		}
		employee := &employeepb.Employee{
			Name:       name,
			Department: department,
			Salary:  salary,
		}
		res, err := client.CreateEmployee(
			context.TODO(),
			&employeepb.CreateEmployeeRequest{
				Employee: employee,
			},
		)
		if err != nil {
			return err
		}
		fmt.Printf("Employee created: %s\n", res.Employee.Id)
		return nil
	},
}

func init() {
	createCmd.Flags().StringP("name", "n", "", "Add an name")
	createCmd.Flags().StringP("department", "d", "", "A department for the employee")
	createCmd.Flags().Int32P("salary", "s", 1, "The salary for the employee")
	createCmd.MarkFlagRequired("name")
	createCmd.MarkFlagRequired("department")
	createCmd.MarkFlagRequired("salary")
	rootCmd.AddCommand(createCmd)
	// Here you will define your flags and configuration settings.

	// Cobra supports Persistent Flags which will work for this command
	// and all subcommands, e.g.:
	// createCmd.PersistentFlags().String("foo", "", "A help for foo")

	// Cobra supports local flags which will only run when this command
	// is called directly, e.g.:
	// createCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

Get Employee command: 

We follow the same steps as the command above. In the init() function, we add the required flag(input parameter). For get employee,  we just need the ID as input. In the cobra.Command we get the data from our flag, create the GetEmployeeRequest and call the RPC GetEmployee
Execute the command below in this folder:
  D:\projects\grpc-go-mongodb-cobra\client>cobra add read
This will create a new file, /cmd/read.go with the layout similar to root.go and the other files described above. Update the code in read.go with the code below:
  package cmd

import (
	"context"
	"fmt"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	"github.com/spf13/cobra"
)

var readCmd = &cobra.Command{
	Use:   "read",
	Short: "Find a Employee by ID",
	Long: `Find a employee by MongoDB Unique identifier.
	
	If no employee is found for the ID it will return a 'Not Found' error`,
	RunE: func(cmd *cobra.Command, args []string) error {
		id, err := cmd.Flags().GetString("id")
		if err != nil {
			return err
		}
		req := &employeepb.GetEmployeeRequest{
			Id: id,
		}
		res, err := client.GetEmployee(context.Background(), req)
		if err != nil {
			return err
		}
		fmt.Println(res)
		return nil
	},
}

func init() {
	readCmd.Flags().StringP("id", "i", "", "The id of the employee")
	readCmd.MarkFlagRequired("id")
	rootCmd.AddCommand(readCmd)
}

Delete Employee command: 

In a similar way we create the Delete Command. Execute the command below in this folder:
  D:\projects\grpc-go-mongodb-cobra\client>cobra add delete
This will create a new file, /cmd/delete.go with the layout similar to root.go and the other files described above. Update the code in delete.go with the code below:
    package cmd

import (
	"context"
	"fmt"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	"github.com/spf13/cobra"
)

// deleteCmd represents the read command
var deleteCmd = &cobra.Command{
	Use:   "delete",
	Short: "Delete a Employee by ID",
	Long: `Delete a employee by MongoDB Unique identifier.
	
	If no employee post is found for the ID it will return a 'Not Found' error`,
	RunE: func(cmd *cobra.Command, args []string) error {
		id, err := cmd.Flags().GetString("id")
		if err != nil {
			return err
		}
		req := &employeepb.DeleteEmployeeRequest{
			Id: id,
		}
		_, err = client.DeleteEmployee(context.Background(), req)
		if err != nil {
			return err
		}
		fmt.Printf("Succesfully deleted the employee with id %s\n", id)
		return nil
	},
}

func init() {
	deleteCmd.Flags().StringP("id", "i", "", "The id of the employee")
	deleteCmd.MarkFlagRequired("id")
	rootCmd.AddCommand(deleteCmd)
}  

Update Employee command:

Next we will work on the Update Employee command. Execute the command below in this folder:
  D:\projects\grpc-go-mongodb-cobra\client>cobra add update
This will create a new file, /cmd/update.go with the layout similar to root.go and the other files described above. Edit the code in update.go with the code below:
  package cmd

import (
	"context"
	"fmt"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	"github.com/spf13/cobra"
)

var updateCmd = &cobra.Command{
	Use:   "update",
	Short: "Find a Employee by ID",
	Long: `Find a employee by MongoDB Unique identifier.
	
	If no employee post is found for the ID it will return a 'Not Found' error`,
	RunE: func(cmd *cobra.Command, args []string) error {
		// Get the flags from CLI
		id, err := cmd.Flags().GetString("id")
		name, err := cmd.Flags().GetString("name")
		department, err := cmd.Flags().GetString("department")
		salary, err := cmd.Flags().GetInt32("salary")
		if err != nil {
			return err
		}
		employee := &employeepb.Employee{
			Id:         id,
			Name:       name,
			Department: department,
			Salary:  salary,
		}
		// Create UpdateEmployeeRequest
		res, err := client.UpdateEmployee(
			context.TODO(),
			&employeepb.UpdateEmployeeRequest{
				Employee: employee,
			},
		)
		if err != nil {
			return err
		}
		fmt.Println(res)
		return nil
	},
}

func init() {
	updateCmd.Flags().StringP("id", "i", "", "The id of the employee")
	updateCmd.Flags().StringP("name", "n", "", "Add an name")
	updateCmd.Flags().StringP("department", "d", "", "A department for the employee")
	updateCmd.Flags().Int32P("salary", "s", 1, "The salary for the employee")
	updateCmd.MarkFlagRequired("id")
	rootCmd.AddCommand(updateCmd)
}

List Employees command: 

Lastly we work on the List Employees command. Execute the command below in this folder:
  D:\projects\grpc-go-mongodb-cobra\client>cobra add list
This will create a new file, /cmd/list.go with the layout similar to root.go and the other files described above. Edit the code in list.go with the code below:
    package cmd

import (
	"context"
	"fmt"
	"io"

	employeepb "github.com/ajtechdeveloper/grpc-go-mongodb-cobra/proto"
	"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
	Use:   "list",
	Short: "Get all employees",
	RunE: func(cmd *cobra.Command, args []string) error {
		req := &employeepb.GetAllEmployeesRequest{}
		// Call GetAllEmployees that returns a stream
		stream, err := client.GetAllEmployees(context.Background(), req)
		if err != nil {
			return err
		}
		// Iterate
		for {
			res, err := stream.Recv()
			// If it is end of the stream, then break the loop
			if err == io.EOF {
				break
			}
			if err != nil {
				return err
			}
			fmt.Println(res.GetEmployee())
		}
		return nil
	},
}

func init() {
	rootCmd.AddCommand(listCmd)
}  

Step 6: Adding Go Module Support

Create a file go.mod at the project folder level and add the contents below in it:
module grpc-go-mongodb-crud

go 1.15     
Now run the command below:
         D:\projects\grpc-go-mongodb-cobra>go mod tidy
         
It will find and download all the dependencies required in this project and append it in the file go.mod. It will also create the file go.sum. After this command runs, here is the updated file go.mod:
module grpc-go-mongodb-crud

go 1.15

require (
	github.com/ajtechdeveloper/grpc-go-mongodb-cobra v0.0.0-20210306143140-7671a81234ec
	github.com/mitchellh/go-homedir v1.1.0
	github.com/spf13/cobra v1.1.3
	github.com/spf13/viper v1.7.1
	go.mongodb.org/mongo-driver v1.4.6
	google.golang.org/grpc v1.36.0
	google.golang.org/protobuf v1.25.0
)  

Run Application

Please make sure that MongoDB is running on your machine. Then run the Server application using the command below:
D:\projects\grpc-go-mongodb-cobra>go run server/main.go
Starting server on port :50051...
Connecting to MongoDB...
Connected to MongoDB
Server successfully started on port :50051
This indicates that the server has started successfully.

Create Employee Command:

To create an employee use the command below in another command prompt:
D:\projects\grpc-go-mongodb-cobra>go run client/main.go create -n "Jane" -d "Technology" -s "15000"
Starting Employee Service Client
Employee created: 604dfac211b424ad42bc6c0d
Here is the employee created in MongoDB:

Get Employee Command:

To get/read an employee use the command below:
D:\projects\grpc-go-mongodb-cobra>go run client/main.go read -i 604dfac211b424ad42bc6c0d
Starting Employee Service Client
employee:{id:"604dfac211b424ad42bc6c0d" name:"Jane" department:"Technology" salary:15000}

Update Employee Command:

To update an employee use the command below:
D:\projects\grpc-go-mongodb-cobra>go run client/main.go update -i "604dfac211b424ad42bc6c0d" -n "Jane" -d "Technology" -s "20000"
Starting Employee Service Client
employee:{id:"604dfac211b424ad42bc6c0d" name:"Jane" department:"Technology" salary:20000}
Here is the employee updated in MongoDB:

List Employees Command:

I have created another employee. Here are the employees in MongoDB:

Now, to list all employees use the command below:
D:\projects\grpc-go-mongodb-cobra>go run client/main.go list
Starting Employee Service Client
id:"604dfac211b424ad42bc6c0d" name:"Jane" department:"Technology" salary:20000
id:"604e02f311b424ad42bc6c0f" name:"Jim" department:"HR" salary:10000

Delete Employee Command:

To delete an employee use the command below:
D:\projects\grpc-go-mongodb-cobra>go run client/main.go delete -i 60
4e02f311b424ad42bc6c0f
Starting Employee Service Client
Successfully deleted the employee with id 604e02f311b424ad42bc6c0f
Here is the employee created in MongoDB:

Stop Server:

In the server command prompt, use Ctrl+C.  This will stop the server as below:
D:\projects\grpc-go-mongodb-cobra>go run server/main.go
Starting server on port :50051...
Connecting to MongoDB...
Connected to MongoDB
Server succesfully started on port :50051

Stopping the server...
Closing MongoDB connection
Done.

Conclusion and GitHub link:

    This tutorial gives a comprehensive overview of creating a gRPC Microservice using Golang and MongoDB Database. The code for the application used in this post is available on GitHub
    Learn the most popular and trending technologies like Blockchain, Cryptocurrency, Machine Learning, Chatbots, Internet of Things (IoT), Big Data Processing, Elastic Stack, React, Highcharts, Progressive Web Application (PWA), Angular 5, GraphQL, Golang, Akka HTTP, Play Framework, Dropwizard,   Docker, Netflix Eureka, Netflix Zuul, Spring Cloud, Spring Boot, Flask and RESTful Web Service integration with MongoDB, Kafka, Redis, Aerospike, MySQL DB in simple steps by reading my most popular blog posts at Software Developer Central.   
    If you like my post, please feel free to share it using the share button just below this paragraph or next to the heading of the post. You can also tweet with #SoftwareDeveloperCentral on Twitter. To get a notification on my latest posts or to keep the conversation going, you can follow me on Twitter or Instagram. Please leave a note below if you have any questions or comments.

Comments

  1. I am happy to find this post Very useful for me, as it contains lot of information. I always prefer to read The Quality and glad I found this thing in you post. Thanks
    A very amazing post thanks for sharing with us.

    ReplyDelete

Post a Comment

Popular Posts

Elasticsearch, Logstash, Kibana Tutorial: Load MySQL Data into Elasticsearch

Dropwizard MySQL Integration Tutorial

Send Email in Java