35.1 The flag Package: Defining and Parsing Flags
Right, so you want to build a command-line tool. Not a script you’ll forget about in a week, but a proper tool. The kind you go install and it feels like a real part of your system. We’re going to start with the absolute basics, the thing that’s been in Go since the beginning: the flag package. It’s the reliable, no-frills pickup truck of CLI parsing. It won’t win any beauty contests, but it’ll always get the job done.
The core concept is brain-dead simple, which is its greatest strength. You define a flag, you parse the command line arguments, and the values from the user are magically (or, more accurately, through the magic of pointers) populated into your variables.
Defining Your First Flags
You define flags by telling the flag package what type of value you expect, the name of the flag, a default value, and a help string. Crucially, you pass it a pointer to where the value should be stored. This is the part that trips everyone up at first. You don’t pass the variable itself; you pass its address. The flag package needs to know where in memory to write the user’s input.
Here’s the classic “Hello, [name]” example, which is the “Hello, World” of CLI tools.
package main
import (
"flag"
"fmt"
)
func main() {
// Define a string flag. Note the & (address-of) operator.
namePtr := flag.String("name", "Guest", "your wonderful name")
loudPtr := flag.Bool("loud", false, "output in all caps")
// This is the line that does all the work of reading os.Args[1:]
flag.Parse()
// Dereference the pointers to get the actual values.
name := *namePtr
if *loudPtr {
name = strings.ToUpper(name)
}
fmt.Printf("Hello, %s!\n", name)
}
You’d run it like ./greeter -name=Gopher -loud, and it would cheerfully shout “HELLO, GOPHER!” back at you. The flag package automatically handles the -h or --help flag for you, generating a usage message. It’s a fantastic, zero-effort feature.
The Other Way: Using Var
The flag.String and flag.Bool functions are convenient, but they return pointers. Sometimes you’d rather just use a variable you’ve already declared. That’s what the flag.StringVar family of functions is for. It’s the same thing, just with a slightly different syntax.
package main
import (
"flag"
"fmt"
"time"
)
func main() {
var url string
var timeout time.Duration
var count int
// Notice we pass the address of our variable here.
flag.StringVar(&url, "url", "", "the URL to fetch (required)")
flag.DurationVar(&timeout, "timeout", 10*time.Second, "request timeout")
flag.IntVar(&count, "count", 1, "number of requests to make")
flag.Parse()
if url == "" {
fmt.Println("Error: the --url flag is required")
flag.Usage()
return
}
fmt.Printf("Fetching %s %d times with a %v timeout...\n", url, count, timeout)
}
I vastly prefer this style. It keeps my variable declarations in one place and avoids littering my code with pointer dereferencing (*namePtr). It just looks cleaner.
The Rough Edges and Pitfalls
Now, let’s be honest. flag is simple, not perfect. Here’s where its age shows.
First, it’s Posix-ly challenged. It doesn’t care about -- versus -. You can use -name or --name interchangeably. This is great for laziness but terrible if you want to follow modern CLI conventions where - is for short flags (-v) and -- is for long flags (--verbose). You can’t do that with flag. It’s a single-dash world.
Second, it has a… quirky relationship with non-flag arguments. The flag.Parse() function will stop parsing as soon as it hits a non-flag argument. This is by design, but it can be surprising.
./myapp -verbose file.txt # This works. 'file.txt' is a non-flag argument.
./myapp file.txt -verbose # This fails. Parse() sees 'file.txt' and stops. It never gets to '-verbose'.
The non-flag arguments are available after you call flag.Parse() via flag.Args(). This leads to the third pitfall: required flags. There is no built-in concept of a required flag. If a user doesn’t provide a -url in our second example, the variable just remains at its default value (an empty string). You must manually check for this, as I did with the if url == "" check. Forgetting to validate your required flags is a very common beginner mistake.
Finally, the help output is functional but, let’s say, “utilitarian.” It works, but it won’t win any design awards. You have some control over it by setting flag.Usage, but you’re basically rewriting it from scratch.
So, when do you use flag? Use it for internal tools, simple scripts, or when you absolutely want zero external dependencies. It’s in the standard library, it’s robust, and it works. For anything more complex—think git commit with its subcommands and layered flags—you’ll want something more powerful. And that’s where our friend Cobra comes in, which we’ll tackle next. But don’t underestimate the humble flag; it’s the solid foundation everything else is built on.