Back to writing

Learning Go: MongoDB CRUD with gRPC

A Gopher in Belgium
A Gopher in Belgium

Go

The last couple of weeks I’ve been busy with my new favorite toy: Go. It is an open source programming language created at google in 2007. It is heavily used at Google and has been battle tested before a public release and by now, is considered to be a very stable language. Go tries to find a middle ground between humans and machines for optimal productivity and performance. Many believe it to be the future of back-end development and micro-services.

While Go is still relatively young, it has been welcomed with open arms in the distributed systems world. Docker, Kubernetes, InfluxDB, Go-Ethereum, … are all built in Go.

Here’s the classical “Why Go?” list:

  • Elegant concurrency using GoRoutines and Channels
  • Multi-core support
  • Very good raw CPU-and-memory-bound performance
  • Types aren’t cumbersome
  • Built in testing framework
  • Static code analysis
  • Compilation / deployment

In this blog I will not teach you how to write “Hello World” in go, instead we’ll write a CRUD API using the official mongodb-go-driver and gRPC. Before continuing make sure you have Golang, MongoDB, RoboMongo (for ease of use) and Protocol Buffers (link is below) installed. To get the most out of this tutorial you should be familiar with Go syntax a little bit as well as pointers.

A Gopher that clearly lifts
A Gopher that clearly lifts

gRPC & HTTP2

REST in peace, there’s a new kid on the block and it’s called gRPC.
gRPC is a modern, HTTP2 based protocol that provides Remote Procedure Call semantics using a strongly-typed binary data format called Protocol Buffers instead of JSON.

Parsing JSON is very CPU intensive and it is estimated that it takes up 45% of all CPU resources for RESTful services. Combined with the upgrades HTTP/2 offers, gRPC API’s are estimated to be 5–25x faster than their RESTful counterpart.

gRPC was built from the ground up to leverage all the good stuff coming with HTTP/2 and has been battle-tested by Google yet again, handling 10 billion requests per second. Here’s a list of fun stuff that HTTP/2 brings to the table:

  • Multiplexing (HTTP/1 opens a TCP connection for every request)
  • Server Push
  • Header compression (plaintext in HTTP/1 and sent with every request)
  • Binary instead of Text (Great match for Protocol Buffers)
  • SSL is a first class citizen

gRPC vs REST

Fully comparing gRPC to REST would be material for a blog post on its own, so here’s an overview:

Illustration from Learning Go: MongoDB CRUD with gRPC

Protocol Buffers

Like mentioned before Protocol Buffers (protobufs in short) are a strongly-typed, language- and platform neutral, binary data format. You can define how you want your data to be structured once in a .proto file and then use special generated source code to easily create server and client stubs for many languages, your .proto file is the canonical format for API contracts. According to google, protobufs are 3–10x smaller than XML and 20–100x faster.

Protocol Buffers are used to defined messages (data, requests and responses) and services (RPC endpoints) in .proto files.

To learn more about protocol buffers, please check the extensive documentation.

Down To Business: Blog Service API

For the remainder of this blog post we will be building a simple CRUD API for a blog service. Since MongoDB just released their official Go driver, we’ll be using MongoDB. We’ll create a server and a small CLI application for the client using Cobra. Let’s get our hands dirty !

  • Cobra does not yet support go-modules for generating subcommands, thus for this tutorial we’ll be using good ol’ $GOPATH :) .
  • This is what your folder structure should look like:
    $GOPATH/grpc-mongo-crud/
    - server
    - client
    - proto
  • Alternatively you could just generate the subcommand files in your $GOPATH and copy-paste the code into your own repository and use relative paths instead (this is the route I took).

You can find the code repository on my github.

Illustration from Learning Go: MongoDB CRUD with gRPC

1. Creating a Proto file

After setting up a new working directory in your $GOPATH, create a new file in the proto folder, let’s call it blog.proto. For simplicity we’ll create seperate messages for each request/response altough some will overlap.

First a little bit of boilerplate that expresses we use protbufs v3, our package is called blog, and our generate go package will be called blogpb (short for blog protocol buffer).

syntax = "proto3";
package blog;
option go_package = "blogpb";
boiler.proto — original code

We’ll need 5 services, each service takes in a request and returns a response:

  • CreateBlog (unary)
  • ReadBlog (unary)
  • UpdateBlog (unary)
  • DeleteBlog (unary)
  • ListBlog (server streaming)
service BlogService {
    rpc CreateBlog(CreateBlogReq) returns (CreateBlogRes);
    rpc ReadBlog(ReadBlogReq) returns (ReadBlogRes);
    rpc UpdateBlog(UpdateBlogReq) returns (UpdateBlogRes);
    rpc DeleteBlog(DeleteBlogReq) returns (DeleteBlogRes);
    rpc ListBlogs(ListBlogReq) returns (stream ListBlogRes);
}
services.proto — original code

CreateBlog, ReadBlog and UpdateBlog will return a Blog item in their responses, so we’ll need a Blog Message Type. Messages can be nested, so we can use our Blog Message inside Request/Response messages.

message Blog {
    string id = 1;
    string author_id = 2;
    string title = 3;
    string content= 4;
}
blogmessage.proto — original code

CreateBlog Request / Response

Both the request and response will consist out of a Blog message, the main difference being that in the request, the ID will be blank as that is created by MongoDB upon creation of the document inside the database. In the response the ID will be filled in.

message CreateBlogReq {
    Blog blog = 1; // Blog id blank
}

message CreateBlogRes {
    Blog blog = 1; // Blog id filled in
}
createblog.proto — original code

UpdateBlog Request / Response

Very similar to CreateBlog, but the Blog message in our Request will already contain the id to find and update that specific item in our database. The response will return the updated document.

message UpdateBlogReq {
    Blog blog = 1;
}

message UpdateBlogRes {
    Blog blog = 1;
}
updateblog.proto — original code

ReadBlog Request / Response

We’ll keep this straightforward and make Blogs searchable by Id only. It is possible though to use id OR title in a single request message using oneof .

message ReadBlogReq {
    string id = 1;
}

message ReadBlogRes {
    Blog blog = 1;
}
readblog.proto — original code

DeleteBlog Request / Response

The request object is identical to ReadBlog, we find the blog by id and remove it (oneof can be used here too). On success we return true otherwise an error is returned.

message DeleteBlogReq {
    string id = 1;
}

message DeleteBlogRes {
    bool success = 1;
}
deleteblog.proto — original code

ListBlogs Request/Response

This one is special, ListBlogs will list all blogs in our database. All our previous services were unary , which means they adhere to the classic request/response mechanism. It works fine when the data is small, but for large data streaming is more optimal. ListBlogs will use server-streaming; for one request message the server will send back multiple blog messages.

message ListBlogsRequest {}

message ListBlogsResponse {
    Blog blog = 1;
}
listblog.proto — original code

Protocol Compiler: Protoc

Now that we have created our .proto definitions it is time to generate our Client and Server stubs in Go. We do this using the protocol buffer compiler combined with the protoc-gen-go plugin.

You can check whether the installation was successful:

$ protoc — version
libprotoc 3.7.1

If it is you can compile your proto file to Go stubs, this will result in a new file in the /proto folder: blog.pb.go

protoc proto/blog.proto --go_out=plugins=grpc:.

I suggest you go through the newly generated file, you’ll see types and interfaces for the server and client, methods to create a new server and client, types to instantiate (requests, responses and blog) and getters for the fields of those structs.

You will also see this line at the top of the file, please take it to heart:

// Code generated by protoc-gen-go. DO NOT EDIT.

2. Server Implementation

Starting / Stopping the server
First things first, we’ll need some boilerplate to create a gRPC server and mongoDB connection, give some feedback to the console and handle server shutdowns by the user through a shutdown hook.

We’ll set all of this up within our main() function, feel free to do some code splitting and cleanup by yourself.

  1. Add filename and linenumber flags when we use the log package
  2. Start our listener
  3. Start a new gRPC server with no service registered yet.
  4. Create a blog service and register it to our new gRPC server.
  5. Connect to MongoDB.
  6. Accept incoming connections on our listener
  7. Handle user initiated server shutdown (CTRL+C)

Now let’s start creating our main() function.

func main() {
	// Configure 'log' package to give file name and line number on eg. log.Fatal
	// Pipe flags to one another (log.LstdFLags = log.Ldate | log.Ltime)
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	fmt.Println("Starting server on port :50051...")

	// Start our listener, 50051 is the default gRPC port
	listener, err := net.Listen("tcp", ":50051")
	// Handle errors if any
	if err != nil {
		log.Fatalf("Unable to listen on port :50051: %v", err)
	}
}
server1.go — original code

Now let’s start a new gRPC server and register our Blog service. To achieve this we’ll use the grpc package as well as the blogpb package created by compiling our .proto file. Our blogpb package has a method called RegisterBlogServiceServer that takes in a pointer to a fresh gRPC server and a BlogServiceServer struct that adheres to the BlogServiceServer interface from our generated blog.pb.go file.

import (
  // other imports ...
  blogpb "../proto"
  // ... 
)

type BlogServiceServer struct {}

func main() {
 // STEP 1 & STEP 2
 // ... 
  
 // Set options, here we can configure things like TLS support 
 opts := []grpc.ServerOption{}
 // Create new gRPC server with (blank) options
 s := grpc.NewServer(opts...)
 // Create BlogService type 
 srv := &BlogServiceServer{}
 // Register the service with the server 
 blogpb.RegisterBlogServiceServer(s, srv)
}
server2.go — original code

Great job ! Now well create our mongodb connection, so make sure you have MongoDB installed as well as the official mongodb-go-driver we’ll be using. If you’ve ever worked with a mongodb driver in another language, you’ll see that it’s quite similar.

// Global variables for db connection , collection and context
var db *mongo.Client
var blogdb *mongo.Collection
var mongoCtx context.Context

func main {
  // STEP 1 & STEP 2 
  // STEP 3 (gRPC server)
  // ... 
  
  // Initialize MongoDb client
	fmt.Println("Connecting to MongoDB...")
  
  // non-nil empty context 
	mongoCtx = context.Background()
  
  // Connect takes in a context and options, the connection URI is the only option we pass for now
	db, err = mongo.Connect(mongoCtx, options.Client().ApplyURI("mongodb://localhost:27017"))
  // Handle potential errors
	if err != nil {
		log.Fatal(err)
	}
  
  // Check whether the connection was succesful by pinging the MongoDB server
	err = db.Ping(mongoCtx, nil)
	if err != nil {
		log.Fatalf("Could not connect to MongoDB: %v\n", err)
	} else {
		fmt.Println("Connected to Mongodb")
	}
  
  // Bind our collection to our global variable for use in other methods
	blogdb = db.Database("mydb").Collection("blog")
}
server3.go — original code

Last but not least we’ll serve the listener to accept incoming connections and handle proper server shutdown using a goroutine and channel to block the main channel until the user presses CTRL+C.

func main() {
  // STEP 1, STEP 2, STEP 3
  // ... 
  
  // 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")

	// Right way to stop the server using a SHUTDOWN HOOK
	// 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 doesn't press CTRL+C a message is not passed and our main routine keeps running
	<-c

	// After receiving CTRL+C Properly stop the server
	fmt.Println("\nStopping the server...")
	s.Stop()
	listener.Close()
	fmt.Println("Closing MongoDB connection")
	db.Disconnect(mongoCtx)
	fmt.Println("Done.")
}
server4.go — original code

And we’re done! You can now start the server running go run server/main.go , make sure however to run sudo service mongod start beforehand, or db.Ping() will return an error. You can then close the server by pressing CTRL+C.

If all went well you should see something like:

Illustration from Learning Go: MongoDB CRUD with gRPC

** Note: You’ll likely need to add empty methods from the next section to not get compile errors, since BlogServiceServer does not adhere to it’s interface (in blog.pb.go) yet.

Creating our Proto Service Handlers

Now that we have our server ready, it’s time to add methods for the services we defined in our .proto file. The methods will take our BlogServiceServer as receiver so it will comply with its interface.

type BlogServiceServer interface {
	CreateBlog(context.Context, *CreateBlogReq) (*CreateBlogRes, error)
	ReadBlog(context.Context, *ReadBlogReq) (*ReadBlogRes, error)
	UpdateBlog(context.Context, *UpdateBlogReq) (*UpdateBlogRes, error)
	DeleteBlog(context.Context, *DeleteBlogReq) (*DeleteBlogRes, error)
	ListBlogs(*ListBlogsReq, BlogService_ListBlogsServer) error
}
interface.go — original code

In the function bodies we’ll generally use the following workflow:

Protbuf Message (Request) → Regular Go Struct → Convert to BSON + Mongo Action → Protobuf Message (Response)

What’s currently missing is the regular go struct to represent a blog, which can be converted to BSON. So let’s create this type above the main function and add some tags, so that the bson package will use the meta information to assign keys.

type BlogItem struct {
	ID       primitive.ObjectID `bson:"_id,omitempty"`
	AuthorID string             `bson:"author_id"`
	Content  string             `bson:"content"`
	Title    string             `bson:"title"`
}

func main() { ... }
BlogItem.go — original code

Now let’s make our CreateBlog method. It takes in a context and a request, in this case CreateBlogReq. First we’ll extract the Blog message from our request message and convert it to a regular go struct. InsertOne() will convert it into BSON for us, using the meta tags we provided. The result from InsertOne() returns the Object ID of the new document inside our collection, which we can then add to our Blog protobuf message to be wrapped in a CreateBlogResponse.

func (s *BlogServiceServer) CreateBlog(ctx context.Context, req *blogpb.CreateBlogReq) (*blogpb.CreateBlogRes, error) {
	// Essentially doing req.Blog to access the struct with a nil check
	blog := req.GetBlog()
	// Now we have to convert this into a BlogItem type to convert into BSON
	data := BlogItem{
		// ID:    Empty, so it gets omitted and MongoDB generates a unique Object ID upon insertion.
		AuthorID: blog.GetAuthorId(),
		Title:    blog.GetTitle(),
		Content:  blog.GetContent(),
	}

	// Insert the data into the database, result contains the newly generated Object ID for the new document
	result, err := blogdb.InsertOne(mongoCtx, data)
	// check for potential errors
	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 blog, first cast the "generic type" (go doesn't have real generics yet) to an Object ID.
	oid := result.InsertedID.(primitive.ObjectID)
  // Convert the object id to it's string counterpart
	blog.Id = oid.Hex()
	// return the blog in a CreateBlogRes type
	return &blogpb.CreateBlogRes{Blog: blog}, nil
}
CreateBlog.go — original code

Next we’ll do ReadBlog(), the ReadBlogReq will take in the Object ID of the blog and retrieve that document from our collection. The MongoDB FindOne() methods takes in a context and a filter, which is a BSON document for which to filter by its keys. We’ll simply have to decode the found document to a regular go struct so we can convert it back to protobuffer messages.

func (s *BlogServiceServer) ReadBlog(ctx context.Context, req *blogpb.ReadBlogReq) (*blogpb.ReadBlogRes, error) {
	// convert 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 := blogdb.FindOne(ctx, bson.M{"_id": oid})
	// Create an empty BlogItem to write our decode result to
	data := BlogItem{}
	// decode and write to data
	if err := result.Decode(&data); err != nil {
		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find blog with Object Id %s: %v", req.GetId(), err))
	}
	// Cast to ReadBlogRes type
	response := &blogpb.ReadBlogRes{
		Blog: &blogpb.Blog{
			Id:       oid.Hex(),
			AuthorId: data.AuthorID,
			Title:    data.Title,
			Content:  data.Content,
		},
	}
	return response, nil
}
ReadBlog.go — original code

Now that we can create and read a blog, we should also be able to delete one. For DeleteBlog we’ll use the mongodb DeleteOne() method, which takes in an Object ID of the document to remove. If the removal is succesful we’ll return a boolean else an error.

func (s *BlogServiceServer) DeleteBlog(ctx context.Context, req *blogpb.DeleteBlogReq) (*blogpb.DeleteBlogRes, error) {
	// Get the ID (string) from the request message and convert it to an Object ID
	oid, err := primitive.ObjectIDFromHex(req.GetId())
	// Check for errors
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("Could not convert to ObjectId: %v", err))
	}
	// DeleteOne returns DeleteResult which is a struct containing the amount of deleted docs (in this case only 1 always)
	// So we return a boolean instead
	_, err = blogdb.DeleteOne(ctx, bson.M{"_id": oid})
	// Check for errors
	if err != nil {
		return nil, status.Errorf(codes.NotFound, fmt.Sprintf("Could not find/delete blog with id %s: %v", req.GetId(), err))
	}
	// Return response with success: true if no error is thrown (and thus document is removed)
	return &blogpb.DeleteBlogRes{
		Success: true,
	}, nil
}
DeleteBlog.go — original code

For the last of our CRUD operations, we’ll need method to update a blog. For simplicity’s sake we are going to entirely replace the existing document inside the collection with the document encoded from our request. In a real-world scenario we’d do nil checks on the arguments so that only filled out fields are actually updated (using the $set keyword from MongoDB). The second quirk is that Mongo’s FindOneAndUpdate() returns the old document, so we’ll have to explicitly tell it to return the new document instead.

func (s *BlogServiceServer) UpdateBlog(ctx context.Context, req *blogpb.UpdateBlogReq) (*blogpb.UpdateBlogRes, error) {
	// Get the blog data from the request
	blog := req.GetBlog()

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

	// Convert the data to be updated into an unordered Bson document
	update := bson.M{
		"authord_id": blog.GetAuthorId(),
		"title":      blog.GetTitle(),
		"content":    blog.GetContent(),
	}

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

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

	// Decode result and write it to 'decoded'
	decoded := BlogItem{}
	err = result.Decode(&decoded)
	if err != nil {
		return nil, status.Errorf(
			codes.NotFound,
			fmt.Sprintf("Could not find blog with supplied ID: %v", err),
		)
	}
	return &blogpb.UpdateBlogRes{
		Blog: &blogpb.Blog{
			Id:       decoded.ID.Hex(),
			AuthorId: decoded.AuthorID,
			Title:    decoded.Title,
			Content:  decoded.Content,
		},
	}, nil
}
UpdateBlog.go — original code

The last method we’ll implement, ListBlogs, is slightly different. Our previous operations were unary CRUD operations, which means they use a classical 1 response for 1 request mechanism.

If we want to list all blogs however, the data payload might grow too large to return it in one response, therefore we’ll implement server streaming through gRPC. For one request to list the blogs, the server will keep streaming blog messages until all blogs have been sent back to the client.

MongoDB is really helpful with this through it’s cursor feature. A cursor in MongoDB is a pointer to the results of a query rather than all of the results in memory.

A pointer to the result set of a query. Clients can iterate through a cursor to retrieve results.

So what we’ll do is iterate over the cursor using cursor.Next() and send each blog as message to the client, until the cursor yields no more results at which point we’ll close the server side stream. It’s more straightforward when you see the code:

func (s *BlogServiceServer) ListBlogs(req *blogpb.ListBlogsReq, stream blogpb.BlogService_ListBlogsServer) error {
	// Initiate a BlogItem type to write decoded data to
	data := &BlogItem{}
	// collection.Find returns a cursor for our (empty) query
	cursor, err := blogdb.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)
		// check error
		if err != nil {
			return status.Errorf(codes.Unavailable, fmt.Sprintf("Could not decode data: %v", err))
		}
		// If no error is found send blog over stream
		stream.Send(&blogpb.ListBlogsRes{
			Blog: &blogpb.Blog{
				Id:       data.ID.Hex(),
				AuthorId: data.AuthorID,
				Content:  data.Content,
				Title:    data.Title,
			},
		})
	}
	// Check if the cursor has any errors
	if err := cursor.Err(); err != nil {
		return status.Errorf(codes.Internal, fmt.Sprintf("Unkown cursor error: %v", err))
	}
	return nil
}
ListBlogs.go — original code

Well done, that’s it for our entire server implementation. You’re on the path to becoming a supergopher ! Next we’ll start working on our CLI client application using Cobra.

Illustration from Learning Go: MongoDB CRUD with gRPC

3. Client Implementation

Cobra
As mentioned earlier, we’ll be making a CLI application for our client using Cobra. It comes with a handy generator to generate boilerplate for us.

The cobra generator doesn’t work with go modules yet, so we’ll generate the command files in our $GOPATH and then copy paste them into our /client folder where we are creating our project (if you started in your $GOPATH from the start, you can run the generator from the /client folder).

First things first, install cobra. This will create an executable in $GOPATH/bin that we can run from anywhere in the terminal (if you have setup Go correctly).

$ go get github.com/spf13/cobra/cobra

Once Cobra is successfully installed, we can initiate a new project.

$ cobra init

This will generate a main.go file and a /cmd folder. The main.go file doesn’t do anything aside from calling execute on the root command, located in /cmd/root.go .

Two things in /cmd/root.go are important to us, the rootCmd struct (a cobra.Command type), which contains the command name and descriptions and the init() function , where we’ll initiate the client. Since sub-commands are bound to the root command, Cobra will always initiate a client before executing the sub-command.

Give your root command a name and some descriptions:

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

And initialize the client:

var cfgFile string

// Client and context global vars for the cmd package
// So they can be used by our subcommands
var client blogpb.BlogServiceClient
var requestCtx context.Context
var requestOpts grpc.DialOption

func init() {
  // initConfig reads in config file and ENV variables
	cobra.OnInitialize(initConfig)
	// After Cobra root config init, initialize the client
	fmt.Println("Starting Blog Service Client")
	// Establish context to timeout after 10 seconds if server does not respond
	requestCtx, _ = context.WithTimeout(context.Background(), 10*time.Second)
	// Establish insecure grpc options (no TLS)
	requestOpts = grpc.WithInsecure()
	// Dial the server, 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 BlogServiceClient with our client connection to the server
	client = blogpb.NewBlogServiceClient(conn)
}
initClient.go — original code

I’m not kidding if I say that was the hardest part of our client, pretty straightforward right? Feel free to play around with Cobra by adding flags or commands, you also have “help” flag out of the box. In our finished application it will look something like this when called:

Illustration from Learning Go: MongoDB CRUD with gRPC

Create Blog Command
Now that our root command is set up, let’s create our first sub-command so that we can actually add blogs to our database.

$ cobra add create

This will create a new file, /cmd/create.go with roughly the same layout as root.go . There’s a struct of type cobra.Command and an init() function.

In the init() function we’ll define our flags. Every blog entry needs an author, title and content so let’s make those flags required as well. To add a flag we first need the Flagset for that command, with StringP(name, shorthand, defaultVal, description) we can add a flag that has a shorthand (eg. ‘a’ for ‘author’). At the end of the init() function we bind our sub-command to the root.

func init() {
	createCmd.Flags().StringP("author", "a", "", "Add an author")
	createCmd.Flags().StringP("title", "t", "", "A title for the blog")
	createCmd.Flags().StringP("content", "c", "", "The content for the blog")
	createCmd.MarkFlagRequired("author")
	createCmd.MarkFlagRequired("title")
	createCmd.MarkFlagRequired("content")
	rootCmd.AddCommand(createCmd)
}
createInit.go — original code

To run a method associated with the create command, our cobra.Command struct will take an additional property RunE. This allows us to execute a method that returns an error, because remember we’re returning internal gRPC errors. The Flagset associated with the command (command.Flags() ) allows us to easily retrieve the arguments given to the flags.

Out of these flags we can then create our request, CreateBlogReq and call the CreateBlog method on the client. Under the hood this will send the RPC request on the wire and return a response.

// createCmd represents the create command
var createCmd = &cobra.Command{
	Use:   "create",
	Short: "Create a new blog post",
	Long: `Create a new blogpost on the server through gRPC. 
	
	A blog post requires an AuthorId, Title and Content.`,
	RunE: func(cmd *cobra.Command, args []string) error {
    // Get the data from our flags 
		author, err := cmd.Flags().GetString("author")
		title, err := cmd.Flags().GetString("title")
		content, err := cmd.Flags().GetString("content")
		if err != nil {
			return err
		}
    // Create a blog protobuffer message  
		blog := &blogpb.Blog{
			AuthorId: author,
			Title:    title,
			Content:  content,
		}
    // RPC call
		res, err := client.CreateBlog(
			context.TODO(),
      // wrap the blog message in a CreateBlog request message
			&blogpb.CreateBlogReq{
				Blog: blog,
			},
		)
		if err != nil {
			return err
		}
		fmt.Printf("Blog created: %s\n", res.Blog.Id)
		return nil
	},
}
createCmd.go — original code

Read Blog Command
That’s pretty easy right? Let’s continue with something even easier and more straightforward, especially now that you’re getting the hang of it! A command to read a blog; this will take in a single flag, the blog Object Id.

Similar to the init() function for our create command, we’ll create our flag for the id and bind the sub-command to our root.

func init() {
	readCmd.Flags().StringP("id", "i", "", "The id of the blog")
	readCmd.MarkFlagRequired("id")
	rootCmd.AddCommand(readCmd)
}
readInit.go — original code

And in our command struct we’ll set the appropriate name and description. Our runE method will be similar as well. We’ll retrieve the id from the flagset, create our request (ReadBlogReq) out of it and invoke the RPC call.

var readCmd = &cobra.Command{
	Use:   "read",
	Short: "Find a Blog post by its ID",
	Long: `Find a blog post by it's mongoDB Unique identifier.
	
	If no blog 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 := &blogpb.ReadBlogReq{
			Id: id,
		}
		res, err := client.ReadBlog(context.Background(), req)
		if err != nil {
			return err
		}
		fmt.Println(res)
		return nil
	},
}
readCmd.go — original code

Update Blog Command
Updating a blog is nearly identical on the client as creating a blog, with the addition of the id flag, because we need to tell mongoDB which document to update of course. Let’s call cobra add update and edit the generated file.

var updateCmd = &cobra.Command{
	Use:   "update",
	Short: "Find a Blog post by its ID",
	Long: `Find a blog post by it's mongoDB Unique identifier.
	
	If no blog 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")
		author, err := cmd.Flags().GetString("author")
		title, err := cmd.Flags().GetString("title")
		content, err := cmd.Flags().GetString("content")

		// Create an UpdateBlogRequest
		req := &blogpb.UpdateBlogReq{
			&blogpb.Blog{
				Id:       id,
				AuthorId: author,
				Title:    title,
				Content:  content,
			},
		}

		res, err := client.UpdateBlog(context.Background(), req)
		if err != nil {
			return err
		}

		fmt.Println(res)
		return nil
	},
}

func init() {
	updateCmd.Flags().StringP("id", "i", "", "The id of the blog")
	updateCmd.Flags().StringP("author", "a", "", "Add an author")
	updateCmd.Flags().StringP("title", "t", "", "A title for the blog")
	updateCmd.Flags().StringP("content", "c", "", "The content for the blog")
	updateCmd.MarkFlagRequired("id")
	rootCmd.AddCommand(updateCmd)
}
UpdateBlog.go — original code

Delete Blog Command
Similar to the read command, this command takes a single flag, the blog id and removes it from the database. The code is nearly identical apart from the invoked client method.

var deleteCmd = &cobra.Command{
	Use:   "delete",
	Short: "Delete a Blog post by its ID",
	Long: `Delete a blog post by it's mongoDB Unique identifier.
	
	If no blog 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 := &blogpb.DeleteBlogReq{
			Id: id,
		}
		// We only return true upon success for other cases an error is thrown
		// We can thus omit the response variable for now and just print something to console
		_, err = client.DeleteBlog(context.Background(), req)
		if err != nil {
			return err
		}
		fmt.Printf("Succesfully deleted the blog with id %s\n", id)
		return nil
	},
}

func init() {
	deleteCmd.Flags().StringP("id", "i", "", "The id of the blog")
	deleteCmd.MarkFlagRequired("id")
	rootCmd.AddCommand(deleteCmd)
}
deleteblogclient.go — original code

List Blogs Command

This one will be slightly different, because if you remember it’s a RPC method that uses server side streaming. Calling ListBlogs will return a grpc client stream. If you wish to know more about this I suggest you check out the interface for it in the gRPC package. But essentially to list all blogs we’ll loop until we reach the end of the stream.

// listCmd represents the read command
var listCmd = &cobra.Command{
	Use:   "list",
	Short: "List all blog posts",
	RunE: func(cmd *cobra.Command, args []string) error {
		// Create the request (this can be inline below too)
		req := &blogpb.ListBlogsReq{}
		// Call ListBlogs that returns a stream
		stream, err := client.ListBlogs(context.Background(), req)
		// Check for errors
		if err != nil {
			return err
		}
		// Start iterating
		for {
			// stream.Recv returns a pointer to a ListBlogRes at the current iteration
			res, err := stream.Recv()
			// If end of stream, break the loop
			if err == io.EOF {
				break
			}
			// if err, return an error
			if err != nil {
				return err
			}
			// If everything went well use the generated getter to print the blog message
			fmt.Println(res.GetBlog())
		}
		return nil
	},
}

func init() {
	rootCmd.AddCommand(listCmd)
}
ListBlogsclient.go — original code

4. Finishing Up

It’s been a long road, but we’re at the finish line. Now you can actually start playing with the CLI application you made. If you’ve been working from your $GOPATH you can go install the CLI app and use the root command directly from the terminal. Otherwise you can call commands like so:

go run client/main.go create -a “Nico Vergauwen” -t “Learning Go” -c “The quick brown fox jumps over the lazy dog”

And that’s it , you’re well on your way to become a king at Go and gRPC ! Thank you for reading, I hope you enjoyed it!

Illustration from Learning Go: MongoDB CRUD with gRPC

Originally published on ITNEXT on 10 May 2019.

Browse all writing