At Lightstep, we automate or build tooling around repeated operational processes (like deployments). In service of this, we have a need for sophisticated command line argument parsing. go-flagsgo-flags gives us the power we need to write useful tools while delegating the complexities of argument parsing to a library.
Some of go-flags features are difficult to discover; this post is intended to be an unofficial guide to some of those features.
The Basics
The inputs for go-flags are the actual command line arguments and a structure describing the expected arguments for the program. go-flags relies on Go struct tags to describe how to interpret the arguments. go-flags then reads and unmarshals command line arguments into the appropriate type.
In the following example, there is one argument, Name, which defaults to the name “Unknown”. It can be set with the –name command line argument.
type Options struct {
Name string `long:"name" description:"Your name, for a greeting" default:"Unknown"`
}
func main() {
var opts Options
parser := flags.NewParser(&opts, flags.Default)
_, err := parser.Parse()
if err != nil {
log.Fatal(err)
}
fmt.Println("Hello", opts.Name)
}
Custom Types
At times you need to pass a command line argument that is fundamentally not a basic type (string, int, etc.). To accommodate this use case, go-flags provides support for types that know how to marshal and unmarshal themselves.
We can consider the example of an IPv4 address, used by a fictional tool that checks for host liveness.
During argument parsing, we want to ensure that the IP address supplied is well-formed IP. We also want to parse the IP so that we can do bulk operations - maybe checking liveness over a set of addresses.
The way to do this is to declare a custom type that implements the MarshalerMarshaler interfaces.
In the following example, we can see what that looks like. In this case, the IP address is supplied in dot-decimal notation (xxx.yyy.zzz.qqq) with the –address command line argument.
The structure of the IP address is validated during parsing, as is the range of each octet.
Note: There is a gotcha here. The IPAddress struct must be a pointer in the options struct. The method receivers on MarshalFlag and UnmarshalFlag must also be pointers.
type IPAddress struct {
net.IP
}
type Options struct {
Address *IPAddress `long:"address" required:"true"`
}
func (a *IPAddress) UnmarshalFlag(arg string) error {
v := net.ParseIP(arg)
if v == nil {
return fmt.Errorf("%q failed to parse", arg)
}
*a = IPAddress{v}
return nil
}
func (a *IPAddress) MarshalFlag() (string, error) {
return a.IP.String(), nil
}
func main() {
var opts Options
parser := flags.NewParser(&opts, flags.Default)
_, err := parser.Parse()
if err != nil {
log.Fatal(err)
}
fmt.Println("Hello", opts.Address)
}
Nested Commands
Nested commands are useful when developing a monolithic binary with many distinct use cases.
The pattern typically goes liketool_binary subcategory command
. We can refactor the above example to use this pattern, as shown in the example below.
There are a few things to notice here. One is that the parser is implicitly the top-level command, and subcommands are directly attached to it (via the AddCommand
calls).
Another thing to notice is that Commands can be nested - the net command is not directly executable (as it has no Execute method); it exists purely for organization.
The last thing to notice is that the command that is actually executable, check-liveness, must implement the CommanderCommander interface. With this implemented, the below program can be run with the arguments net check-liveness --address 10.0.0.1
.
type CheckLivenessCommand struct {
Address *IPAddress `long:"address" required:"true"`
}
func (c *CheckLivenessCommand) Execute(args []string) error {
fmt.Println("Checking liveness for", c.Address)
return nil
}
func addNetCommands(parser *flags.Parser) error {
cmd, err := parser.AddCommand(
"net",
"networking utils",
"Utilities developed to ease operation and debugging of network connected services.",
&struct{}{},
)
if err != nil {
return err
}
const livenessHelp = "Checks the liveness of the specified IP"
cmd, err = cmd.AddCommand(
"check-liveness",
livenessHelp, // short (--help)
livenessHelp, // long (manpages)
&CheckLivenessCommand{},
)
if err != nil {
return err
}
return nil
}
func main() {
var opts struct{}
parser := flags.NewParser(&opts, flags.Default)
err := addNetCommands(parser)
if err != nil {
log.Fatal(err)
}
_, err = parser.Parse()
if err != nil {
log.Fatal(err)
}
}
Restricted Argument Values
go-flags provides a facility to restrict the set of legal values for an argument. These are called choices, and can be used on basic types like strings. This is useful to restrict the services your tool operates on, or the environments it operates in.
The example below illustrates the usage of choice. go-flags will validate that Service is one of the two choices.
type Options struct {
Service string `long:"service" choice:"proxy" choice:"database"`
}
Common Gotcha — required:”false”
The idiomatic way to set an argument as required is to apply the struct tag required:”true”
. The gotcha here is that any string - not just “true” - will set the tag as required. In particular, required:”false”
will mark the argument as required.
Command Line Arguments in Practice
go-flags has allowed us to build full featured command line tools where argument parsing just works. Hopefully the above lessons learned will make your usage a little bit smoother.
If you want to learn more about how we build software and write code at Lightstep, check out our walkthrough on how we built our recent Slack integrationbuilt our recent Slack integration.
Interested in joining our team? See our open positions herehere.
Explore more articles

Monitoring Apache with OpenTelemetry and Lightstep
Andrew Gardner | May 2, 2023Continue your observability journey by ingesting metrics from Apache and sending them to Lightstep.
Learn moreLearn more
Monitoring MySQL with OpenTelemetry and Lightstep
Andrew Gardner | Apr 11, 2023Learn how to ingest metrics from MySQL and send them to Lightstep.
Learn moreLearn more
Monitoring NGINX with OpenTelemetry and Lightstep
Robin Whitmore | Apr 6, 2023Learn how to start ingesting metrics from NGINX and send them to Lightstep for more intelligent analysis and monitoring.
Learn moreLearn moreLightstep sounds like a lovely idea
Monitoring and observability for the world’s most reliable systems