Intro πŸšͺ

Go is an open source programming language supported by Google. It has gained it’s grounds in the programming world due to it’s simplicity and speed. It is a statically typed language with a syntax similar to C. It is a great language for building command line interfaces (CLI), Cloud/DevOp tools, Networking and Backend server applications. In this article, I will be giving an overview of how I built the gomeasure tool with Go.

The Journey 🚲

I’ve always wanted to build a CLI tool but ideas have not been forthcoming until one fateful day at work meeting. There was a need to calculate the weight (files count) of directories in a project. Hence, I wrote a simple Go script which evolved into the gomeasure CLI tool.

Why I built with Go? πŸ€”

I chose Go because of the following reasons:

I was learning go at the time

I’ve developed the habit of learning at least one new programming language every year. I started learning Go in 2022 and I wanted to build something with it.

Go has a rich standard library and inbuilt tools for building fast elegant CLIs.

The flag, os and fmt packages are enough for building a simple CLI tool. This provides flag parsing out of the box compared to other languages like Java where I would have parsed arguments manually. Anyways, I extended these packages with the github.com/spf13/cobra package which made building even easier.

Collaboration and community.

I learnt and built the tool with a friend, Ahmed Helali. We both have a passion for open source and we wanted to build something together.

Small binaries

Go binaries are very small in size. The typical size of a Go binary is 2-5 MB. This is a great advantage because the binary has a small memory footprint (we don’t want a memory hog), there is no dependency overhead and binaries can be easily shared.

Cross-platform support (Windows, Linux, Mac)

Go is a compiled language and it compiles to a single binary file which can be run on any platform. This is unlike interpreted languages like Python and JavaScript which require a runtime environment to run. This makes Go a good choice for building CLIs. I built the tool for various platforms in a CI/CD environment and it worked on all platforms without issues.

Structure of a CLI program πŸ—

As developers, we use CLI programs every day ranging from git, docker, kubectl, etc. A typical CLI program has the following structure:
<program> [command] [arguments] [flags] e.g.
gomeasure line /path/to/file -v
I employed the following model for the gomeasure CLI tool:

  • In the docs, <> means a required argument and [] means an optional argument.
  • [command] is a sub-command of the program. e.g. line in gomeasure line is a sub-command of gomeasure.
  • sub-commands have required arguments and optional flags. e.g. gomeasure line /path/to/file -v has a required argument /path/to/file and an optional flag -v.
    Here, it is obvious that the arguments are necessary for the subcommand to carry out the expected action while the flags tells it how to.
  • gomeasure has a help sub-command which displays the help message for the program. e.g. gomeasure help displays the help message for the program. The help message is also accessible from the --help flag. e.g. gomeasure line --help displays the help message for the line sub-command.
  • Flags or options that have a single dash - are short flags. e.g. -v is a short flag and is equivalent to --verbose.
  • Short flags can be combined. e.g. -v -s is equivalent to -vs.
  • Flags or options that have a double dash -- are long flags. e.g. --verbose is a long flag.

Introduction to Cobra 🐍

Cobra is a package built by Steve Francia for building powerful modern CLI applications in Go. It provides a simple interface to create powerful modern CLI interfaces and it is used in production grade softwares like Kubernetes, Hugo and Github CLI, to name a few. Cobra provides a generator to create a CLI skeleton which can be customized to suit your needs. I used the generator to create the gomeasure CLI tool.

Cobra out of the box use the structure described above.

Creating gomeasure with Cobra and Go πŸ› 

Project structure

The project structure is as follows:

.
β”œβ”€β”€ LICENSE
β”œβ”€β”€ README.md
β”œβ”€β”€ bin
β”‚   └── gomeasure
β”œβ”€β”€ .goreleaser.yaml
β”œβ”€β”€ cmd
β”‚   β”œβ”€β”€ file.go
β”‚   β”œβ”€β”€ line.go
β”‚   └── root.go
β”œβ”€β”€ dist/
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ main.go
β”œβ”€β”€ pkg
β”‚   β”œβ”€β”€ runner.go
β”‚   └── runner_test.go
  • LICENSE is the license file.
  • README.md is the readme file.
  • bin is the directory where the compiled binary is stored.
  • cmd is the directory where the CLI commands are stored.
  • dist is the directory where the compiled binaries for various platforms are stored.
  • go.mod is the go module file.
  • go.sum is the go module checksum file.
  • main.go is the entry point of the program.
  • pkg is the directory that contains the business logic of the software.
  • .goreleaser.yaml is the goreleaser configuration file.

Cobra and the cmd/ directory

The scope of cobra is limited to the cmd/ directory because this is where each sub-command is defined and flags are parsed.

  • The most important class in the cobra package is the cobra.Command class. It is used to define a sub-command and its flags. An example of how we used it to define the file sub-command is shown below:
var fileCmd = &cobra.Command{
	Use: "file <directory>",

	Short: "processes the number of files in a directory",
	Long:  `gomeasure file processes and returns the number of files in a directory / project.`,
	Run: func(cmd *cobra.Command, args []string) {
		// ...
	},
}
  • Cobra also helps to parse flags and it automatically resolves conflicts between flags. An example of how we used it to parse the --verbose flag is shown below:
// cmd/root.go
rootCmd.PersistentFlags().BoolVarP(&isVerbose, "verbose", "v", false, "--desc--")

You can check out the cobra documentation at https://github.com/spf13/cobra.

The runner.go file

The runner.go file contains the business logic of the program. It contains the Runner struct which contains fields that helps to specify what the program has to do.
This file also contains the Result struct which is returned from the Run method of the Runner struct. The Result struct contains the result of the program execution.

Deployment and CI/CD with Github Actions

I used Github Actions to build and deploy the program. The workflow is as follows:

  • The workflow is triggered when a new release is created.
  • The workflow builds the program for various platforms using goreleaser. The `.goreleaser.yaml file contains instructions on how and where to deploy releases.
  • The workflow then creates a new release and uploads the compiled binaries to the github releases section and on brew (for macOS).

Roadmap for gomeasure πŸ›£

More subcommands πŸ€–

I plan to improve the tool by adding more sub-commands and more flags to the existing sub-commands to make it more useful.

Unit Testing omg πŸ₯΅

I plan to add unit tests to the *.go files. It is actually a highly recommended practice to write unit tests in the Go community.

Docker image 🐳

I plan to create a docker image release for the tool so that it can be used in a docker container.


Conclusion

I’ve made the project open source and I would love to see contributions from the community.

Don’t forget to star🌟 the repository on Github if you found gomeasure useful.