The Go flag package: strange but good?
Go has a sizeable and useful standard library. It covers many common use cases, reducing the need for dependencies. (Unlike some other languages that shall remain unnamed. cough cough js) It exposes well documentated APIs that compose nicely. And it's source code serves as an example of idiomatic Go.
The Duplications
While Go's standard library is pretty great, the flag package is a bit strange. Most of the exposed functions are duplicated: the methods of FlagSet are also exposed as top-level functions. These functions wrap the methods of the default flag set which is an exported global variable. That seems a bit un-Go to me. (Although, to be fair, it's replicated in a few packages so maybe it's not so un-Go after all.)
Maybe exposing a default FlagSet along with its methods makes the package more convenient. It might make the package a tad easier to use? But I don't think that's the reason for it or at least it shouldn't be. Go always prefers explicit over implicit. Besides, how hard it is to make a new flag set when you need it? There's a good chance you will have to do so anyway as you quickly need multiple flag sets.
gofmt's style is no one's favorite, yet gofmt is everyone's favorite
— Rob Pike, https://www.youtube.com/watch?v=PAAkCSZUG1c&t=523s
Though, there is another thing why flag is the way it is. Go is, among all other things, pragmatic. Like the above proverb, the same principle applies to Go as a whole. Go doesn't offer redundant features. It doesn't try to please everyone and implement just any feature. On the contrary, it usually even tries to limit the features. And in doing so, it has became our favorite language. Because it limits us. It offers an easy and obvious way to do the thing you want. Not a million ways.
One case where I regularly use the default exported flag set is in tests. The first time I tried this, I was left wondering how it works. I simply defined a flag in the test file, ran the test, and it just worked? No need to call flag.Parse() or anything? That's magic whereas there shouldn't be any in Go!
But there is, there is a bit of magic even in Go. However, it's not there just because. It has a reason and it's useful. When you look at how it works, you again, slowly realize that everything just comes together.
The testing package uses the exposed default flag set. And because it's exposed, you have full access to it! You can add your own custom flags and call flag.Parse() yourself if you want. By exposing the default flag set, `flag` enables a useful use case solely through existing packages you already know.
The Flag Set Feature Set
Take a stroll down GitHub and you will find a bunch of CLI libraries. Some with, really, a lot of stars. But why would people want other libraries when they could use flag?
Flag has, in true Go fashion, a limited set of features. And with these features it's not hard to built a CLI. But you will probably need some boilerplate, so many flock to libraries with "nicer" APIs. These libraries can remove the need for boilerplate but often come with other features we don't necessarily need. So, wouldn't it be better to use the flag package? It comes with the language, has good documentation, and, after you get to know it, isn't all that strange. Or is it?
I prefer not to use dependencies when unnecessary. They're another thing to maintain and another thing that's likely to break. That's because, most dependencies don't guarantee backwards compatibility like Go does. And unlike with a myriad of external dependencies, most people will already be familiar with the standard packages. Not to mention, their existing integrations. You cannot add a custom flag to your tests with other packages.
Using flag is fairly straigthforward. Except, again, we run into some flag's quirks. Let's look at an example.
args := os.Args[1:]
if len(args) == 0 {
// no command, exit with code 2 (invalid usage)
}
cmd, args := args[0], args[1:]
switch cmd {
case "a":
fs := flag.NewFlagSet("a", flag.ExitOnError)
repeat := fs.Bool("repeat", false, `print "aa" instead of "a"`)
_ := fs.Parse(args) // ExitOnError means flag exits for us
if *repeat {
fmt.Println("aa")
} else {
fmt.Println("a")
}
case "b":
fs := flag.NewFlagSet("b", flag.ExitOnError)
// as in a
default:
// unknown command, exit with code 2 (invalid usage)
}
This is mostly classic, run-of-the-mill Go. But what's going on with flag.ExitOnError? Since when doesn't Go return an error but exits instead? Flag has three error handling modes:
- ContinueOnError—returns an error (like normal Go)
- ExitOnError—exits
- PanicOnError—panics
This is another strange flag quirk. I have yet to find another standard Go package that incorporates similar behaviour.
The Double Fang
Finally, we come to the last flag quirk: there is no "--". Or so I thought. In fact, either "-" or "--" may be used; they are equivalent. I always assumed that Go would support getopt-like flags, with short and long options. Regardless, I have come to love that it's not like getopt.
Again, Go makes things simple; it dared to streamline flags.
Why do we even need both long and short flags?
Most of the time we don't.
Thinking about all of this, tar comes to mind.
It gives me every possible way to pass a flag, yet I need to
tldr it every time.
They should just use tar fml
instead of tar xvf
.
As for why the "-" and "--" both can be used, I don't know. It makes little sense to me. Maybe Go is liberal in what it accepts? Or maybe it was liberal in what it accepted but stayed that way because of backwards compatibility? (See The Harmful Consequences of the Robustness Principle if you are interested how and why that happens.)
The Conclusion
The Go flag package is different. It's not what you would expect it to be with its global variables, exiting on errors, and double dashes. It's the most un-Go package in the standard library. Despite this, I have come to love its quirks and simplicity.
Although I wonder why flag is the way it is? Did the Go developers write it before they developed or rather refined the Go philosophy? Sifting through the Go source code, one can find a lot flag usages. Maybe flag is the result of a practial need for flags in these other parts of Go? Or is it what its developers thought was most pragmatic?
It turns out it's solely a simplified Google's flag package with a straightforward syntax (except for the "-" and "--"?).
As the author of the flag package, I can explain. It's loosely based on Google's flag package, although greatly simplified (and I mean greatly). I wanted a single, straightforward syntax for flags, nothing more, nothing less.
— Rob 'Commander' Pike, https://groups.google.com/g/golang-nuts/c/3myLL-6mA94/m/VUkLtSOyS-YJ
Comments at https://old.reddit.com/r/golang/comments/r46y3s/the_flag_package_strange_but_good/.
You can see this page's source written in Touch and the config used.