Building a CRUD Microservice using gRPC in Go

10 Apr 2023

gRPC is a powerful tool for building distributed systems and microservices and is a great choice for developers who need a high-performance, reliable communication mechanism between their applications.

Woman in futuristic auditorium

In this article, we’ll walk you through the process of building a Microservice using gRPC in Go, starting with setting up your development environment and defining the service’s API using Protocol Buffers. You’ll learn how to implement the service’s business logic, as well as the gRPC server and client. By the end of this article, you’ll have a good understanding of how to design and build a Microservice using gRPC in Go.

Introduction to Microservices and gRPC

Microservices is an architectural style that structures an application as a collection of small, independent services, each running its own process and communicating with lightweight mechanisms, often an HTTP API. The goal of microservices is to enable fast delivery of large and complex applications, increase efficiency and scalability, and allow for a more agile development process.

The main advantage of microservices is the ability to develop, deploy, and scale services independently, leading to faster release cycles, increased deployment efficiency, and reduced downtime. Additionally, small, cross-functional teams can develop and maintain microservices, leading to faster innovation and reduced time-to-market.

However, there are also some disadvantages of microservices to consider. The complexity of communication between services can increase, leading to the need for additional management and orchestration tools. Additionally, there may be increased operational overhead and increased latency due to the need for inter-service communication.

gRPC is a high-performance, open-source framework for building scalable, modern, and fast APIs. It uses the Protocol Buffers data format and supports various programming languages. gRPC is based on the concept of remote procedure calls (RPCs), where a client can make a request to a server and receive a response as if it were calling a local function.

gRPC uses HTTP/2 for transport, allowing for bi-directional streaming and flow control, which enables faster and more efficient communication between services. The use of Protocol Buffers provides a compact and efficient way to serialize data, leading to faster and more reliable communication.

One of the main advantages of using gRPC is its high performance and low latency. Additionally, its use of Protocol Buffers can lead to smaller payload sizes, resulting in faster transmission times and reduced network bandwidth usage. Another advantage is the ability to easily generate client and server code in various programming languages, which can reduce the time and effort required to implement new services.

However, there are also some disadvantages to using gRPC. One is the limited browser support, as gRPC currently relies on HTTP/2, which is not widely supported in browsers. Additionally, gRPC may have a steeper learning curve for developers unfamiliar with Protocol Buffers or remote procedure calls. Another consideration is that gRPC requires more upfront design and planning than REST-based APIs. This can lead to a longer development cycle and require a deeper understanding of gRPC and Protocol Buffers.

Finally, it’s important to note that gRPC may not be the best fit for all use cases. In situations where REST-based APIs are sufficient, it may be an over-engineering solution. Additionally, gRPC may not be well-suited for applications that require real-time updates or notifications, as the request/response nature of gRPC may not provide the necessary real-time updates.

Setting up the development environment

Setting up the development environment for a gRPC project in Go is crucial in getting started with the project. The following steps outline the process for installing the required components and setting up the development environment:

Setup a module environment for the gRPC project by running the following go mod command in the project directory (for example, pieces-grpc):

go mod init github.com/IkehAkinyemi/grpc-service

Note to replace IkehAkinyemi with your GitHub username if it’s your preferred repository, and if not, do well to choose the preferred repository link.

Next in the setup is installing the Protocol Buffers compiler and gRPC plugins for Go. The first step in setting up the development environment is to install the Protocol Buffers compiler (protoc) using the macOS package manager:

brew install protobuf

Check here if you’re using a different OS environment to follow along with the tutorial. Then check if the installation was successfully executed by running the below command:

protoc --version  #libprotoc 3.21.12

Next, let’s complete the setup by installing some Go plugins for the project. The easiest way to do this is to use the Go package manager, go get, to install the required components. The following command can be run in a terminal to install the necessary components:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc

After downloading the plugins, let’s define a simple CRUD service. The first step is to define the service API using the Protocol Buffers language. This is done by creating a .proto file, which defines the service and its methods. The following example shows how to define a CRUD (Create, Read, Update, Delete) service API:

//File: pieces-grpce/api/user.proto
syntax = "proto3";

option go_package = "/gen";

service UserService {
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
    rpc ReadUser(ReadUserRequest) returns (ReadUserResponse) {}
    rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) {}
    rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {}
}
message CreateUserRequest {
    string name = 1;
    int32 age = 2;
}
message CreateUserResponse {
    string id = 1;
}
message ReadUserRequest {
    string id = 1;
}
message ReadUserResponse {
    string name = 1;
    int32 age = 2;
}
message UpdateUserRequest {
    string id = 1;
    string name = 2;
    int32 age = 3;
}
message UpdateUserResponse {
}
message DeleteUserRequest {
    string id = 1;
}
message DeleteUserResponse {
}

This Protocol Buffer message definition defines a gRPC service named UserService with 4 remote procedure calls (RPC): CreateUser, ReadUser, UpdateUser, and DeleteUser. Each RPC maps a request message to a response message. Each RPC’s request and response messages are defined as separate message types, such as CreateUserRequest and CreateUserResponse. The messages contain fields, such as name and age, in the CreateUserRequest, each with a unique field number tag. The syntax specified at the beginning of the file is protobuf version 3. To learn more about the Protocol Buffer message definition, check here.

protoc --proto_path=. --go_out=. \
  --go-grpc_out=. \
  ./api/*.proto

Use protoc with the following options: --proto_path=., --go_out=., --go-grpc_out=., to compile the .proto files in the api directory and generate .pb.go files in the same directory as the package name specified using option go_package in the input files, preserving their sub-directory structure if any, but not creating the output directory. Check the ./gen folder to see the generated Go code. To learn more about the generated code, check here.

With the development environment set up and the .proto file defining the service API, the next step is implementing the gRPC service and client in Go. The following outlines the process for implementing the service and client, using a simple business logic that operates CRUD on an in-memory map object type in Go.

Let’s start with the implementation of the server by defining the struct that would serve as both the controller and in-memory repository:

// File: pieces-grpc/internal/handler/grpc.go
package handler

import (
  "github.com/IkehAkinyemi/grpc-service/gen"
)

type Handler struct {
  gen.UnimplementedUserServiceServer
  users map[string]user
}

type user struct {
  Name string
  Age  int32
}

func New() *Handler {
  return &Handler{users: make(map[string]user)}
}

The code defines a handler package in Go. This package contains a Handler struct that implements the generated gen.UnimplementedUserServiceServer interface. The Handler struct also has a users field of type map[string]user.

The user struct represents a user with fields Name and Age. The New function returns a pointer to a new instance of the Handler struct, which is an implementation of the gen.UnimplementedUserServiceServer interface.

The users field is a map from strings (representing the user’s unique identifier) to instances of the user struct. The gen.UnimplementedUserServiceServer interface represents the generated code for a gRPC service, and the Handler struct implements the methods in this interface. The business logic for the gRPC service will be added to the methods in this implementation.

Let’s complete the business logic by implementing the methods defined by gen.UnimplementedUserServiceServer for the Handler struct, thereby overriding the gen.UnimplementedUserServiceServer’s inherited methods.

// File: pieces-grpc/internal/handler/grpc.go
package handler

import (
  "context"
  "github.com/IkehAkinyemi/grpc-service/gen"
  "github.com/google/uuid"
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
)

// --snip--

func (h *Handler) CreateUser(ctx context.Context, req *gen.CreateUserRequest) (*gen.CreateUserResponse, error) {
  id := uuid.New().String()
  h.users[id] = user{
      Name: req.Name,
      Age:  req.Age,
  }
  return &gen.CreateUserResponse{Id: id}, nil
}

func (h *Handler) ReadUser(ctx context.Context, req *gen.ReadUserRequest) (*gen.ReadUserResponse, error) {
  u, ok := h.users[req.Id]
  if !ok {
      return nil, status.Errorf(codes.NotFound, "user not found")
  }
  return &gen.ReadUserResponse{
      Name: u.Name,
      Age:  u.Age,
  }, nil
}

func (h *Handler) UpdateUser(ctx context.Context, req *gen.UpdateUserRequest) (*gen.UpdateUserResponse, error) {
  u, ok := h.users[req.Id]
  if !ok {
      return nil, status.Errorf(codes.NotFound, "user not found")
  }
  u.Name = req.Name
  u.Age = req.Age
  h.users[req.Id] = u
  return &gen.UpdateUserResponse{}, nil
}

func (h *Handler) DeleteUser(ctx context.Context, req *gen.DeleteUserRequest) (*gen.DeleteUserResponse, error) {
  delete(h.users, req.Id)
  return &gen.DeleteUserResponse{}, nil
}

The code defines four methods for the Handler struct, each of which implements a different gRPC API endpoint for performing CRUD (Create, Read, Update, Delete) operations on users.

CreateUser generates a new user ID using the uuid package, adds a new user to the users map using this ID as the key, and returns a CreateUserResponse containing the new user ID.

ReadUser retrieves the user with the given ID from the users map, and returns a ReadUserResponse containing the user’s name and age. If the user is not found, the method returns an error with a NotFound status.

UpdateUser retrieves the user with the given ID from the users map, updates the user’s name and age, and adds the updated user back to the map. If the user is not found, the method returns an error with a NotFound status.

DeleteUser deletes the user with the given ID from the users map.

These methods represent the business logic for the gRPC service and handle the various requests from clients to perform CRUD operations on the in-memory user data.

Next. let’s implement the server logic, connecting it to the business logic.

// File: pieces-grpc/cmd/server/main.go
package main
import (
  "fmt"
  "log"
  "net"
  "github.com/IkehAkinyemi/grpc-service/gen"
  "github.com/IkehAkinyemi/grpc-service/internal/handler"
  "google.golang.org/grpc"
  "google.golang.org/grpc/reflection"
)

func main() {
  log.Println("Starting the sever")
  port := 50051
  listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }
  h := handler.New()
  srv := grpc.NewServer()
  reflection.Register(srv)
  gen.RegisterUserServiceServer(srv, h)
  if err := srv.Serve(listener); err != nil {
    panic(err)
  }
}

The above snippet is the main function that starts a gRPC server on port 8080. It creates a new handler object and uses it to register the UserService with the gRPC server. The server then listens on the specified port, and if there is an error, it will log the error and stop.

Next, let’s setup a client CLI program to communicate with the gRPC server like below:

// File: pieces-grpc/cmd/client/main.go
package main

import (
  "log"
  "github.com/IkehAkinyemi/grpc-service/gen"
  "google.golang.org/grpc"
  "google.golang.org/grpc/credentials/insecure"
)
func main() {
  conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
  if err != nil {
    log.Fatalf("did not connect: %v", err)
  }
  defer conn.Close()
  client := gen.NewUserServiceClient(conn)
}

This is the main Go code for the gRPC client. The code creates a client connection to a gRPC server running on localhost:50051 using an insecure transport channel. If the connection is successful, the conn variable holds the connection to the server. The code creates a client to the gRPC server’s UserService by calling the gen.NewUserServiceClient function and passing it the conn variable. The client is then ready to send requests to the UserService. If there’s an error while creating the connection, the code logs the error message and terminates the client. The defer statement closes the connection to the server when the client is done.

Next, let’s setup utility functions to send requests to the server using the client to create, read, update or delete a user:

// File: pieces-grpc/cmd/client/user_utils.go

// createUser inserts a random user into the in-memory repository.
func createUser(ctx context.Context, client gen.UserServiceClient) string {
  user := generateUser()
  resp, err := client.CreateUser(ctx, &gen.CreateUserRequest{
    Name: user.Name,
    Age:  int32(user.Age),
  })
  if err != nil {
    log.Fatalf("could not create user: %v", err)
  }
  return resp.Id
}

// readUser returns a user with the id parameter.
func readUser(ctx context.Context, client gen.UserServiceClient, id string) user {
  readResp, err := client.ReadUser(ctx, &gen.ReadUserRequest{Id: id})
  if err != nil {
    log.Fatalf("could not read user: %v", err)
  }
  return user{
    Name: readResp.Name,
    Age:  readResp.Age,
  }
}

// updateUser return an updated user info.
func updateUser(ctx context.Context, client gen.UserServiceClient, id string) user {
  _, err := client.UpdateUser(ctx, &gen.UpdateUserRequest{
    Id:  id,
    Age: randomInt(1, 100),
  })
  if err != nil {
    log.Fatalf("could not update user: %v", err)
  }
  return readUser(ctx, client, id)
}

// deleteUser deletes a user with the id parameter.
func deleteUser(ctx context.Context, client gen.UserServiceClient, id string) {
  _, err := client.DeleteUser(ctx, &gen.DeleteUserRequest{Id: id})
  if err != nil {
    log.Fatalf("could not delete user: %v", err)
  }
  log.Printf("Deleted User with ID: %s", id)
}

These are 4 functions that perform CRUD operations on a user in a gRPC service.

With these functions defined, let’s update the main function to perform either create, read, update, or delete based on the flag argument:

// File: pieces-grpc/cmd/client/main.go
--snip--
func main() {
  var create bool
  var read bool
  var update bool
  var delete bool
  flag.BoolVar(&create, "c", false, "Create a user request")
  flag.BoolVar(&read, "r", false, "Retrieve a user request")
  flag.BoolVar(&update, "u", false, "Update a user request")
  flag.BoolVar(&delete, "d", false, "Delete a user request")
  flag.Parse()

  --snip--

  userID := string(flag.Arg(0))
  if create {
    userID := createUser(ctx, client) // Create User
    log.Printf("Created User with ID: %s", userID)
  } else if read {
    newUser := readUser(ctx, client, userID) // Read User
    log.Printf("Read User: %+v", newUser)
  } else if update {
    updatedUser := updateUser(ctx, client, userID) // Update User
    log.Printf("Updated User %+v with ID: %s", updatedUser, userID)
  } else if delete {
    deleteUser(ctx, client, userID) // Delete User
  }
}

Now we have the client ready, use the following commands to spin the gRPC server, and make requests to it using the client like below:

go run ./cmd/server/*

# open a new terminal, run the following
go run ./cmd/client/* -c # create user

go run ./cmd/client/* -r <user_id> # retrieve a user using returned id

go run ./cmd/client/* -u <user_id> # update user

go run ./cmd/client/* -d <user_id> # delete a user

Conclusion

In conclusion, this article has discussed the implementation of a gRPC service for user management. The server is implemented in Go and uses gRPC to handle client requests and the underlying communication between the client and server. The service is defined in a .proto file, and the generated code is used to implement the server and client. The implementation of the service provides basic CRUD operations for user management, such as creating, reading, updating, and deleting users. The gRPC client establishes a connection to the server and uses the generated client code to interact with the service. For the entire codebase, please check here.