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:
-
Fast: Golang is a compiled language, the code written is directly
translated into formats that any processor understands.
-
Lightweight and minimalist language: The formal Go language
specification is only a few pages
-
Memory safety: The compiler manages memory safety by introducing
“Bounds Check” points in the code that guarantees safe access to the
memory.
- Native Garbage collection
-
Native concurrency: The Go language has Goroutines, that are functions
that can run independently and simultaneously.
-
Structural typing: Go is strongly and statically typed with no
implicit conversions. It has simple type inference in assignments
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:
-
Due to the use of Protocol Buffers and HTTP/2, it is faster when
compared to REST while sending and receiving data.
-
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.
-
Performance and efficiency is much better when compared to REST. REST
uses JSON as payload. Parsing JSON is very CPU intensive.
-
Provides support for code generation.
Requirements to Run the Application:
- Golang
- Protocol Buffers
-
Visual Studio Code or any IDE that supports working on Golang
code.
- 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.
- Fast: Golang is a compiled language, the code written is directly translated into formats that any processor understands.
- Lightweight and minimalist language: The formal Go language specification is only a few pages
- Memory safety: The compiler manages memory safety by introducing “Bounds Check” points in the code that guarantees safe access to the memory.
- Native Garbage collection
- Native concurrency: The Go language has Goroutines, that are functions that can run independently and simultaneously.
- Structural typing: Go is strongly and statically typed with no implicit conversions. It has simple type inference in assignments 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:
- Due to the use of Protocol Buffers and HTTP/2, it is faster when compared to REST while sending and receiving data.
- 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.
- Performance and efficiency is much better when compared to REST. REST uses JSON as payload. Parsing JSON is very CPU intensive.
- Provides support for code generation.
Requirements to Run the Application:
- Golang
- Protocol Buffers
- Visual Studio Code or any IDE that supports working on Golang code.
- 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.
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:
- Kubernetes
- Hugo
- 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 mainThis 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:
- Initialize the client and connect to the server on localhost:50051.
- Instantiate the EmployeeServiceClient with our client connection to the server
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 createThis 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 updateThis 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 listThis 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.15Now run the command below:
D:\projects\grpc-go-mongodb-cobra>go mod tidyIt 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 :50051This 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: 604dfac211b424ad42bc6c0dHere 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 604e02f311b424ad42bc6c0fHere 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.
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.
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
ReplyDeleteA very amazing post thanks for sharing with us.