In this blog post, we will explore the nuances of structuring CLIs using the flag package in Golang; a topic rarely discussed.

Writing Idiomatic Go CLIs


We will cover the basics of the Go Flag Package, creating custom flag types, and setting up automatic help generation to enhance the usability of our CLI.

For the purpose of this blog, we will be referencing a health check application that takes a set of URLs and checks them for uptime. The implementation details will be left to the reader (as they are not the focus of this post). If you do want further guidance on how to build that, you can check out the byteSizeGo CLI course where we build this application and cover many more crucial topics.

Understanding the Input Structure of CLI Tools

Before we dive into the specifics of the Go Flag Package, it’s crucial to understand the input structure of CLI tools. This knowledge shapes how we interact with our command line applications and how we use the Go Flag Package to maintain user inputs.

The way we structure our input follows a logical pattern, akin to the structure of a sentence in our everyday language. In the realm of CLIs, we start with the application name to set the context for our command. Depending on the design of your application, you might prefer different input structures. For instance, “noun-verb” versus “verb-noun” structures can both be valid, much like “open the door” versus “door open.”

Adding Specificity with Flags

Adjectives add flavor to our language, and in CLI, these adjectives come in the form of flags. Flags refine our commands, provide specifics, or modify the behavior of our command. It’s like asking someone to open the door slowly—the “slowly” adds a layer of specificity to our request.

Crafting a CLI command is almost poetic. It’s about choosing the right elements and structuring them in a way that’s clear, efficient, and intuitive. Whether you’re a user trying to understand a command or a developer designing one, understanding the structure is paramount.

Building Our CLI Tool

With the principles of application names, verbs, nouns, and flags in mind, let’s build a command that’s clear and intuitive.

Step 1: Basic Setup

To start, we need to create the basic setup for a new Go application. Open your terminal in VS Code (or any other IDE) and initialize a new module using:

go mod init <module_name>

Next, create a new file and define the package as main.

Step 2: Adding Flags

One of the first things we need is a URL flag. We can add this using the flag package:

package main

import (
	"flag"
)

func main() {
	var url string
	flag.StringVar(&url, "url", "", "URL to check")
}

Step 3: Function to Check URL

We will create a function checkURL that takes a string value and prints out the status of the URL:

func checkURL(url string) {
    // TODO: you should implement this or watch our course to learn how :)
    fmt.Println("Checking URL:", url)
}

Step 4: Handling Errors and Output

Always ensure your error messages are user-friendly. Print error messages to standard error and any successful outputs to standard output.

Step 5: Common Flags

Some common flags you’ll see in a CLI are silent and verbose. The silent flag suppresses standard output, while the verbose flag overrides the silent flag if both are added together.

var silent, verbose bool
flag.BoolVar(&silent, "silent", false, "Run in silent mode without standard output")
flag.BoolVar(&verbose, "verbose", false, "Run in verbose mode (overrides silent mode)")

Step 6: Threshold Flag

We can add a threshold flag to notify if the time to get a request exceeds a specific threshold:

var threshold float64
flag.Float64Var(&threshold, "threshold", 0.5, "Threshold value for considering a response to be too slow (in seconds)")

Step 7: Retries Flag

Finally, we can add a retries flag to specify the number of times our CLI should try getting the URL before giving up:

var retries int
flag.IntVar(&retries, "retries", 3, "Number of retries for a failed request")

Step 8: Parsing Flags

The flag.Parse function processes and assigns the flag values to variables:

flag.Parse()

Step 9: Custom Flag Types

To check more than one URL, we need to create a custom flag type for a list of URLs. This involves defining a type that satisfies the flag.Value interface:

package main

import (
	"flag"
	"strings"
)

type URLList []string

func (u *URLList) String() string {
	return strings.Join(*u, ",")
}

func (u *URLList) Set(value string) error {
	*u = append(*u, strings.Split(value, ",")...)
	return nil
}

var urls URLList

func main() {
	flag.Var(&urls, "urls", "Comma-separated list of URLs to check")
}

Step 10: Custom Help Messages

A user-friendly CLI tool provides clear guidance. Our tool can automatically generate help messages, but we can also customize our help output:

flag.Usage = func() {
    fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
    fmt.Fprintf(flag.CommandLine.Output(), "This tool performs health checks on specified URLs.\n")
    flag.PrintDefaults()
}

Step 11: Main Function

Finally, we need to implement the main function to use the flags and perform the health checks. The complete code should look as follows. You can also play with it in the Go Playground

package main

import (
	"flag"
	"fmt"
	"os"
	"strings"
)

type URLList []string

func (u *URLList) String() string {
	return strings.Join(*u, ",")
}

func (u *URLList) Set(value string) error {
	*u = append(*u, strings.Split(value, ",")...)
	return nil
}

var urls URLList

func checkURL(url string) {
	// TODO: you should implement this or watch our course :)
	fmt.Println("Checking URL:", url)
}

func main() {
	var url string
	flag.StringVar(&url, "url", "", "URL to check")

	var retries int
	flag.IntVar(&retries, "retries", 3, "Number of retries for a failed request")

	var threshold float64
	flag.Float64Var(&threshold, "threshold", 0.5, "Threshold value for considering a response to be too slow (in seconds)")

	var silent, verbose bool
	flag.BoolVar(&silent, "silent", false, "Run in silent mode without standard output")
	flag.BoolVar(&verbose, "verbose", false, "Run in verbose mode (overrides silent mode)")

	flag.Var(&urls, "urls", "Comma-separated list of URLs to check")

	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
		fmt.Fprintf(flag.CommandLine.Output(), "This tool performs health checks on specified URLs.\n")
		flag.PrintDefaults()
	}
	flag.Parse()

	if url != "" {
		checkURL(url)
	} else if len(urls) > 0 {
		for _, u := range urls {
			checkURL(u)
		}
	} else {
		flag.Usage()
		os.Exit(1)
	}
}

If you run this code, you’ll get the help output which looks as follows:

This tool performs health checks on specified URLs.
  -retries int
    	Number of retries for a failed request (default 3)
  -silent
    	Run in silent mode without standard output
  -threshold float
    	Threshold value for considering a response to be too slow (in seconds) (default 0.5)
  -url string
    	URL to check
  -urls value
    	Comma-separated list of URLs to check
  -verbose
    	Run in verbose mode (overrides silent mode)

Conclusion

Today, we’ve covered essential aspects of the Go Flag Package, from basic to advanced usage, including creating custom flag types and enhancing user guidance with custom help messages.

While the Go standard package provides a solid foundation for building basic CLI tools, more advanced CLIs might benefit from popular CLI frameworks like Cobra. We cover off Cobra usage in module 2 of our course, but keep your eye out for a blog on the topic too. If you want to be notified when that comes out, you can subscribe to our newsletter and receive a free guide to 60+ companies hiring Go Engineers right now.

See you next time!