Writing a Simple gRPC Application in Golang From Scratch

simple micro-service implementation

gRPC is a modern open-source, high-performance Remote Procedure Call (RPC) framework that can run in any environment. It is best suitable for internal communication between microservices.

Let’s build a simple gRPC application from scratch to understand gRPC better.

Building a Microservice in Golang

Photo by ASTERISK KWON on Unsplash

Let’s use an example of a book service to demonstrate how to use the gRPC framework. The complete code is on GitHub

This service will have five methods:

  • CreateBook

  • RetrieveBook

  • UpdateBook

  • DeleteBook

  • ListBook

Define A Protobuf File

gRPC uses the Protobuf .proto file format to define the messages, services,and some aspects of the code generation.

Messages are underlying interchange format.

In our example, we define a message type Book that contains a field name, author, price, etc.

syntax = "proto3";package api.v1;import "google/protobuf/timestamp.proto";
option go_package = "github.com/jerryan999/book-service/api/v1";message Book {
    int64 bid = 1;
    string title = 2;
    string author = 3;
    string description = 4;
    string language = 8;
    google.protobuf.Timestamp finish_time = 9;
}...

There are a couple of things to note in this short example.

  • We're using the latest version of the protobuf syntax proto3

  • Protobufs has its’ own timestamp type, so we need to use it properly.

If we want to use message types with an RPC (Remote Procedure Call) system, we must define an gRPC service interface in a .proto file.

Service defines rpc methods for remote calling

service BookService { rpc CreateBook(CreateBookRequest) returns (CreateBookResponse) {}; rpc RetrieveBook(RetrieveBookRequest)returns(RetrieveBookResp) {};

 rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) {};

 rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse) {};

 rpc ListBook(ListBookRequest) returns (ListBookResponse) {};
}message CreateBookRequest {
    Book book = 1;
}message CreateBookResponse {
    int64 bid = 1; 
}message RetrieveBookRequest {
    int64 bid = 1;
}message RetrieveBookResponse {
    Book book = 1;
}
...

The Compiler Generates Codes

Let’s execute the following command to generate Golang code.

protoc ./api/v1/*.proto \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
--proto_path=.

The above command will generate two files:

  • One for serializing the messages (book.pb.go) using protobuf.

  • The other file (book_grpc.pb.go) consists of some code for the gRPC client and server code.

Business Code

In this part, we define a Book struct and aBookRepository interface. They are used to describe the business logic of the book service.

package internalimport (
 "context"
 "time"
)type BookId int64type Book struct {
 Bid         BookId    `json:"bid"`
 Title       string    `json:"title"`
 Author      string    `json:"author"`
 Description string    `json:"description"`
 Language    string    `json:"language"`
 FinishTime  time.Time `json:"finishTime"`
}type BookRepository interface {
 CreateBook(ctx context.Context, book *Book) (BookId, error)
 RetrieveBook(ctx context.Context, bid BookId) (*Book, error)
 UpdateBook(ctx context.Context, book *Book) error
 DeleteBook(ctx context.Context, bid BookId) error
 ListBook(ctx context.Context, offset int64, limit int64) ([]*Book, error)
}

Different BookRepository interface implementations can be used in different persistence layers (e.g., MongoDB, Postgres, etc.). This strategy is called Dependency Injection (DI).

Mongo Persistence layers

In this part, we will create a new type, MongoBookRepositoryWhich implements the BookRepository interface.

...const (
 bookCollection = "books"
)type MongoBookRepository struct {
 counter    BookId // increment book id
 mu         sync.Mutex
 collection *mongo.Collection
}func NewMongoBookRepository(db *mongo.Database) *MongoBookRepository {
 return &MongoBookRepository{
  collection: db.Collection(bookCollection),
 }
}func (r *MongoBookRepository) CreateBook(ctx context.Context, book *Book) (BookId, error) {...}
func (r *MongoBookRepository) RetrieveBook(ctx context.Context, bid BookId) (*Book, error) {...}

This new struct has three fields:

  • A collectionfield, which is a reference to a MongoDB collection.

  • A counterfield, which is used to generate unique IDs for the books.

  • A mufield, Which is a mutex. It ensures that only one operation is performed when incrementing the counter variable.

gRPC Server Implementation

The gRPC server implements the BookServiceServer interface from book_grpc.pb.go. We can use it to start a gRPC server in the next part.

...type grpcServer struct {
 BookRepository BookRepository
 api.UnimplementedBookServiceServer
}func NewRPCServer(repository BookRepository) *grpc.Server {
 srv := grpcServer{
  BookRepository: repository,
 }
 gsrv := grpc.NewServer()
 api.RegisterBookServiceServer(gsrv, &srv)
 return gsrv}func (s *grpcServer) CreateBook(ctx context.Context, req *api.CreateBookRequest) (*api.CreateBookResponse, error) {
 book := &Book{
  Bid:         0,
  Title:       req.Book.GetTitle(),
  Author:      req.Book.GetAuthor(),
  Description: req.Book.GetDescription(),
  Language:    req.Book.GetLanguage(),
  FinishTime:  req.Book.GetFinishTime().AsTime(),
 }
 bid, error := s.BookRepository.CreateBook(ctx, book)
 if error != nil {
  return nil, status.Errorf(codes.InvalidArgument, error.Error())
 }
 return &api.CreateBookResponse{Bid: int64(bid)}, nil
}func (s *grpcServer) RetrieveBook(ctx context.Context, req *api.RetrieveBookRequest) (*api.RetrieveBookResponse, error) {...}...

Starting the Server

Starting a gRPC server is very simple. here is a code fragment from internal/server.go

...
port := ":8080"
listen, err := net.Listen("tcp", port)...  
// mongo db
client, _ := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
db := client.Database("testing")// mongo repository
var repository internal.BookRepository = NewMongoBookRepository(db)// gRPC server start
srv := NewRPCServer(repository)
if err := srv.Serve(listen); err != nil {
  log.Fatalf("Failed to serve: %v", err)
}

Call from client

we can find the gRPC Client code in cmd/client/main.go. Please refer to that file for more details.

Wrapping Up

I hope this example of implementing the gRPC application with Go helped you understand the gRPC better.