Getting started with gRPC in Golang

gRPC is a modern communication framework that can run in any environment and helps connect services efficiently. This post explores what gRPC is and how to get started by building a user management service using gRPC, MongoDB and Golang.

avatar

Demola Malomo

May 23 2023

8 min read

avatar

Microservices architecture is one of the preferred methods of building scalable and robust applications. It involves breaking large applications into smaller components that are well-defined, performs a specific task and uses sets of application programming interface (API) for their communication.

Communication is an essential part of microservices; it plays an important role in letting services talk to each other within the larger application context. Some examples of protocol microservices use to communicate with each other includes HTTP, gRPC, message brokers, etc.

In this article, we will explore what gRPC is and how to get started by building a user management service using gRPC, MongoDB and Golang.

What is gRPC?

gRPC is a modern communication framework that can run in any environment and helps connect services efficiently. It was introduced in 2015 and governed by the Cloud Native Computing Platform (CNCF). Beyond efficiently connecting services across a distributed system, mobile applications, frontend to backend, etc., it supports health checking, load balancing, tracing, and authentication.

gRPC offers a fresh perspective to developers building medium to complex applications as it can generate client and server bindings for multiple languages. The following are some of its benefits:

Service definition

gRPC uses Protocol Buffers as its interface description language, similar to JSON and provides features like authentication, cancellation, timeouts, etc.

Lightweight and performant

gRPC definitions are 30 percent smaller than JSON definitions and are 5 to 7 times faster than a traditional REST API.

Multiple platform support

gRPC is language agnostic and has automated code generation for client and server-supported languages.

Scalable

From the developer’s environment to production, gRPC is designed to scale millions ler seconds requests.

Getting started

Now that we understand gRPC's importance in building scalable applications, let’s build a user management service with gRPC, MongoDB, and Golang. The project source code can be found here.

Prerequisites

To fully grasp the concepts presented in this tutorial, the following are required:

  • Basic understanding of Golang
  • Basic understanding of Protocol Buffer
  • Protocol Buffer compiler installed
  • A MongoDB account to host the database. Signup is completely free.
  • Postman or any gRPC testing application

Project and Dependencies setup

To get started, we need to navigate to the desired directory and run the command below in our terminal:

1mkdir grpc_go && cd grpc_go

This command creates a Golang project called grpc_go and navigates into the project directory.

Next, we need to initialize a Go module to manage project dependencies by running the command below:

1go mod init grpc_go

This command will create a go.mod file for tracking project dependencies.

We proceed to install the required dependencies with:

1go get google.golang.org/grpc go.mongodb.org/mongo-driver/mongo github.com/joho/godotenv google.golang.org/protobuf

google.golang.org/grpc is the Golang implementation of gRPC.

go.mongodb.org/mongo-driver/mongo is a driver for connecting to MongoDB.

github.com/joho/godotenv is a library for managing environment variables.

google.golang.org/protobuf is the Golang implementation of Protocol Buffers.

Defining the user management Protocol Buffer and compilation

To get started, we need to define a Protocol Buffer to represent all the operations and responses involved in the user management service. To do this, first, we need to create a proto folder in the root directory, and in this folder, create a user.proto file and add the snippet below:

1syntax = "proto3"; 2package user; 3option go_package = "grpc_go/proto"; 4 5service UserService { 6 rpc GetUser (UserRequest) returns (UserResponse); 7 rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); 8 rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse); 9 rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse); 10 rpc GetAllUsers (Empty) returns (GetAllUsersResponse); 11} 12 13message UserRequest { 14 string id = 1; 15} 16 17message UserResponse { 18 string id = 1; 19 string name = 2; 20 string location = 3; 21 string title = 4; 22} 23 24message CreateUserRequest { 25 string name = 2; 26 string location = 3; 27 string title = 4; 28} 29 30message CreateUserResponse { 31 string data = 1; 32} 33 34message UpdateUserRequest { 35 string id = 1; 36 string name = 2; 37 string location = 3; 38 string title = 4; 39} 40 41message UpdateUserResponse { 42 string data = 1; 43} 44 45message DeleteUserRequest { 46 string id = 1; 47} 48 49message DeleteUserResponse { 50 string data = 1; 51} 52 53message Empty {} 54 55message GetAllUsersResponse { 56 repeated UserResponse users = 1; 57}

The snippet above does the following:

  • Specifies the use of proto3 syntax
  • Declares user as the package name
  • Uses the go_package option to define the import path of the package and where the generated code will be stored
  • Creates a service to Create, Read, Edit, and Delete (CRUD) a user and their corresponding responses as messages.

Lastly, we need to compile the user.proto file using the command below:

1protoc --go_out=. --go_opt=paths=source_relative \ 2 --go-grpc_out=. --go-grpc_opt=paths=source_relative \ 3 proto/user.proto

The command above uses the Protocol Buffer compiler to generate Golang server and client code by specifying the relative part and using the user.proto file.

To avoid errors, we must ensure we add Golang to the path.

On successful compilation, we should see user_grpc.pb.go and user.pb.go files added to the proto folder. These files contain the gRPC-generated server and client code. In this article, we will only use the server code.

gRPC-generated files

Using the generated code from gRPC in our application

With the compilation process done, we can start using the generated code in our application.

Database setup and integration

First, we need to set up a database and a collection on MongoDB as shown below:

database setup

We also need to get our database connection string by clicking on the Connect button and changing the Driver to Go.

Secondly, we must modify the copied connection string with the user's password we created earlier and change the database name. To do this, we need to create a .env file in the root directory and add the snippet copied:

1MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority

Sample of a properly filled connection string below:

1MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5akf.mongodb.net/projectMngt?retryWrites=true&w=majority

Thirdly, we need to create a helper function to load the environment variable using the github.com/joho/godotenv library. To do this, we need to create a configs folder in the root directory; here, create an env.go file and add the snippet below:

1package configs 2 3import ( 4 "log" 5 "os" 6 "github.com/joho/godotenv" 7) 8 9func EnvMongoURI() string { 10 err := godotenv.Load() 11 if err != nil { 12 log.Fatal("Error loading .env file") 13 } 14 return os.Getenv("MONGOURI") 15}

Fourthly, we need to create a model to represent our application data. To do this, we need to create user_model.go file in the same configs folder and add the snippet below:

1package configs 2 3import "go.mongodb.org/mongo-driver/bson/primitive" 4 5type User struct { 6 Id primitive.ObjectID `json:"id,omitempty"` 7 Name string `json:"name,omitempty" validate:"required"` 8 Location string `json:"location,omitempty" validate:"required"` 9 Title string `json:"title,omitempty" validate:"required"` 10}

Lastly, we need to create a db.go file to implement our database logic in the same configs folder and add the snippet below:

1package configs 2 3import ( 4 "context" 5 "fmt" 6 "log" 7 "time" 8 9 "go.mongodb.org/mongo-driver/bson" 10 "go.mongodb.org/mongo-driver/bson/primitive" 11 "go.mongodb.org/mongo-driver/mongo" 12 "go.mongodb.org/mongo-driver/mongo/options" 13) 14 15type dbHandler interface { 16 GetUser(id string) (*User, error) 17 CreateUser(user User) (*mongo.InsertOneResult, error) 18 UpdateUser(id string, user User) (*mongo.UpdateResult, error) 19 DeleteUser(id string) (*mongo.DeleteResult, error) 20 GetAllUsers() ([]*User, error) 21} 22 23type DB struct { 24 client *mongo.Client 25} 26 27func NewDBHandler() dbHandler { 28 client, err := mongo.NewClient(options.Client().ApplyURI(EnvMongoURI())) 29 if err != nil { 30 log.Fatal(err) 31 } 32 33 ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 34 err = client.Connect(ctx) 35 if err != nil { 36 log.Fatal(err) 37 } 38 39 //ping the database 40 err = client.Ping(ctx, nil) 41 if err != nil { 42 log.Fatal(err) 43 } 44 45 fmt.Println("Connected to MongoDB") 46 47 return &DB{client: client} 48} 49 50func colHelper(db *DB) *mongo.Collection { 51 return db.client.Database("projectMngt").Collection("User") 52} 53 54func (db *DB) CreateUser(user User) (*mongo.InsertOneResult, error) { 55 col := colHelper(db) 56 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 57 defer cancel() 58 59 newUser := User{ 60 Id: primitive.NewObjectID(), 61 Name: user.Name, 62 Location: user.Location, 63 Title: user.Title, 64 } 65 res, err := col.InsertOne(ctx, newUser) 66 67 if err != nil { 68 return nil, err 69 } 70 71 return res, err 72} 73 74func (db *DB) GetUser(id string) (*User, error) { 75 col := colHelper(db) 76 var user User 77 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 78 defer cancel() 79 80 objId, _ := primitive.ObjectIDFromHex(id) 81 82 err := col.FindOne(ctx, bson.M{"_id": objId}).Decode(&user) 83 84 if err != nil { 85 return nil, err 86 } 87 88 return &user, err 89} 90 91func (db *DB) UpdateUser(id string, user User) (*mongo.UpdateResult, error) { 92 col := colHelper(db) 93 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 94 defer cancel() 95 96 objId, _ := primitive.ObjectIDFromHex(id) 97 98 update := bson.M{"name": user.Name, "location": user.Location, "title": user.Title} 99 result, err := col.UpdateOne(ctx, bson.M{"_id": objId}, bson.M{"$set": update}) 100 101 if err != nil { 102 return nil, err 103 } 104 105 return result, err 106} 107 108func (db *DB) DeleteUser(id string) (*mongo.DeleteResult, error) { 109 col := colHelper(db) 110 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 111 defer cancel() 112 113 objId, _ := primitive.ObjectIDFromHex(id) 114 115 result, err := col.DeleteOne(ctx, bson.M{"_id": objId}) 116 117 if err != nil { 118 return nil, err 119 } 120 121 return result, err 122} 123 124func (db *DB) GetAllUsers() ([]*User, error) { 125 col := colHelper(db) 126 var users []*User 127 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 128 defer cancel() 129 130 results, err := col.Find(ctx, bson.M{}) 131 132 if err != nil { 133 return nil, err 134 } 135 136 for results.Next(ctx) { 137 var singleUser *User 138 if err = results.Decode(&singleUser); err != nil { 139 return nil, err 140 } 141 users = append(users, singleUser) 142 } 143 144 return users, err 145}

The snippet above does the following:

  • Imports the required dependencies
  • Line 15 - 21: Defines a dbHandler interface that describes all the associated functions in our user management service
  • Line 23 - 25: Creates a DB struct with col property that will implement the dbHandler interface
  • Line 27 - 48: Creates a NewDBHandler constructor function that ties the DB struct and the dbHandler interface it implements by initializing the database connection to MongoDB and returning the appropriate response
  • Line 50 - 52: Creates a colHelper function that accepts the database connection by specifying the database name and associated collection
  • Line 54 - 145: Creates the required methods CreateUser, GetUser, UpdateUser, DeleteUser, and GetAllUsers with a DB pointer receiver and returns the appropriate responses. The methods also use the appropriate methods from MongoDB to perform the required operations

Integrating the database logic with gRPC-generated code

With our database logic setup, we can use the methods to create our application handlers. To do this, we need to create a service folder; here, create a user_service.go file and add the snippet below:

1package services 2 3import ( 4 "context" 5 "grpc_go/configs" 6 pb "grpc_go/proto" 7) 8 9var db = configs.NewDBHandler() 10 11type UserServiceServer struct { 12 pb.UnimplementedUserServiceServer 13} 14 15func (service *UserServiceServer) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) { 16 resp, err := db.GetUser(req.Id) 17 18 if err != nil { 19 return nil, err 20 } 21 22 return &pb.UserResponse{Id: resp.Id.String(), Name: resp.Name, Location: resp.Location, Title: resp.Title}, nil 23} 24 25func (service *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { 26 newUser := configs.User{Name: req.Name, Location: req.Location, Title: req.Title} 27 _, err := db.CreateUser(newUser) 28 29 if err != nil { 30 return nil, err 31 } 32 33 return &pb.CreateUserResponse{Data: "User created successfully!"}, nil 34} 35 36func (service *UserServiceServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) { 37 newUser := configs.User{Name: req.Name, Location: req.Location, Title: req.Title} 38 _, err := db.UpdateUser(req.Id, newUser) 39 40 if err != nil { 41 return nil, err 42 } 43 44 return &pb.UpdateUserResponse{Data: "User updated successfully!"}, nil 45} 46 47func (service *UserServiceServer) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*pb.DeleteUserResponse, error) { 48 _, err := db.DeleteUser(req.Id) 49 50 if err != nil { 51 return nil, err 52 } 53 54 return &pb.DeleteUserResponse{Data: "User details deleted successfully!"}, nil 55} 56 57func (service *UserServiceServer) GetAllUsers(context.Context, *pb.Empty) (*pb.GetAllUsersResponse, error) { 58 resp, err := db.GetAllUsers() 59 var users []*pb.UserResponse 60 61 if err != nil { 62 return nil, err 63 } 64 65 for _, v := range resp { 66 var singleUser = &pb.UserResponse{ 67 Id: v.Id.String(), 68 Name: v.Name, 69 Location: v.Location, 70 Title: v.Title, 71 } 72 users = append(users, singleUser) 73 } 74 75 return &pb.GetAllUsersResponse{Users: users}, nil 76}

The snippet above does the following:

  • Imports the required dependencies
  • Initializes the database using the NewDBHandler constructor function
  • Creates a UserServiceServer that implements the gRPC-generated UserServiceServer interface inside the user_grpc.pb.go file
  • Creates the required methods by passing the UserServiceServer struct as a pointer and returning the appropriate responses as generated by gRPC

Creating the server

With that done, we can create the application gRPC server by creating a main.go file in the root directory and add the snippet below:

1package main 2 3import ( 4 "log" 5 "net" 6 7 pb "grpc_go/proto" 8 "grpc_go/services" 9 "google.golang.org/grpc" 10) 11func main() { 12 lis, err := net.Listen("tcp", "[::1]:8080") 13 if err != nil { 14 log.Fatalf("failed to listen: %v", err) 15 } 16 17 grpcServer := grpc.NewServer() 18 service := &services.UserServiceServer{} 19 20 pb.RegisterUserServiceServer(grpcServer, service) 21 err = grpcServer.Serve(lis) 22 23 if err != nil { 24 log.Fatalf("Error strating server: %v", err) 25 } 26}

The snippet above does the following:

  • Imports the required dependencies
  • Specifies the application port using the in-built net package
  • Creates an instance of gRPC server using the NewServer method and specifies the associated service using the UserServiceServer struct
  • Register the service implementation with the gRPC server
  • Starts the server using the Serve method by passing the required port and handling errors appropriately

With that done, we can test our application by running the command below in our terminal.

1go run main.go

Testing with Postman

With our server up and running, we can test our application by creating a new gRPC Request.

Create new

Select gRPC Request

Input grpc://[::1]:8080 as the URL, select the Import a .proto file option and upload the user.proto file we created earlier.

URL and upload proto file

With that done, the corresponding method will be populated and we can test them accordingly.

getAllUsers getAUser

We can also validate that our gRPC server works by checking our MongoDB collection

Collection

Conclusion

This post discussed what gRPC is, its role in building scalable applications and how to get started by building a user management service with Golang and MongoDB. Beyond what was discussed above, gRPC offers robust techniques around authentication, error handling, performance, etc.

These resources might be helpful:

Related posts