In Go, the flag package is the standard way to handle command-line arguments. It allows you to define options like -port=8080 or -verbose that users can pass when running your program.
The Three-Step Workflow
To use flags correctly, you must follow this specific order:
- Define: Tell Go what flags to look for (names, types, and defaults).
- Parse: Call
flag.Parse(). This is the most important step; without it, your variables will never receive the values from the command line. - Access: Use the values in your logic.
Defining Flags (Two Ways)
There are two common ways to define a flag: returning a pointer or binding to an existing variable.
Method A: Returning a Pointer
This is the most common method. The function returns a pointer to the value.
// Returns a *string
namePtr := flag.String("name", "Guest", "Name to greet")
// Returns an *int
portPtr := flag.Int("port", 8080, "Port number")
Method B: Binding to a Variable (Var suffix)
Use this if you already have a variable declared (common when using structs).
var mode string
flag.StringVar(&mode, "mode", "fast", "Operation mode")
Common Flag Types & Examples
The package supports all basic Go types and even time durations.
| Type | Function Example | How to use in Terminal |
|---|---|---|
| String | flag.String("s", "val", "desc") | -s="hello" or -s hello |
| Integer | flag.Int("n", 10, "desc") | -n 50 |
| Boolean | flag.Bool("v", false, "desc") | -v (sets to true) or -v=false |
| Duration | flag.Duration("d", 5*time.Second, "desc") | -d 10m or -d 1h30m |
Here is a script that combines these concepts into a working tool.
package main
import (
"flag"
"fmt"
"time"
)
func main() {
// 1. Define flags
user := flag.String("user", "admin", "The username to log in as")
retries := flag.Int("retries", 3, "Number of connection attempts")
debug := flag.Bool("debug", false, "Enable debug logging")
timeout := flag.Duration("timeout", 5*time.Second, "Max wait time")
// 2. PARSE (CRITICAL: Must be called before accessing flags)
flag.Parse()
// 3. Use values (Note the '*' for pointers)
fmt.Printf("Logging in as: %s\n", *user)
fmt.Printf("Retries: %d\n", *retries)
fmt.Printf("Timeout: %v\n", *timeout)
if *debug {
fmt.Println("DEBUG: Detailed logs enabled.")
}
}
Items to remember
- Automatic Help: Run your program with
-hor--help. Theflagpackage automatically generates a clean usage menu based on your descriptions. - Boolean Values: Unlike other types, a boolean flag doesn’t need a value. Just saying
-debugsets it totrue. If you want to set it tofalseexplicitly, you must use an equals sign:-debug=false. - Positional Args: Anything provided after the flags (like file names) can be accessed using
flag.Args().
Subcommands
In Go, a Subcommand is handled by creating independent sets of flags using flag.NewFlagSet. This allows your program to have different options depending on which “action” the user chooses.
When you use subcommands, the standard flag.Parse() isn’t enough because the flags change based on the first argument. Instead, you check the first argument (the command) and then parse only the flags associated with that command.
Here is how you can create a tool with two subcommands: fetch and send.
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// 1. Define the subcommands
fetchCmd := flag.NewFlagSet("fetch", flag.ExitOnError)
sendCmd := flag.NewFlagSet("send", flag.ExitOnError)
// 2. Define flags unique to 'fetch'
url := fetchCmd.String("url", "", "The URL to fetch from")
// 3. Define flags unique to 'send'
dest := sendCmd.String("dest", "localhost", "The destination address")
secure := sendCmd.Bool("secure", false, "Use SSL/TLS")
// 4. Check if a subcommand was even provided
if len(os.Args) < 2 {
fmt.Println("expected 'fetch' or 'send' subcommands")
os.Exit(1)
}
// 5. Switch on the subcommand and parse accordingly
switch os.Args[1] {
case "fetch":
fetchCmd.Parse(os.Args[2:]) // Parse everything AFTER the 'fetch' word
fmt.Printf("Fetching from URL: %s\n", *url)
case "send":
sendCmd.Parse(os.Args[2:]) // Parse everything AFTER the 'send' word
fmt.Printf("Sending to: %s (Secure: %v)\n", *dest, *secure)
default:
fmt.Println("Unknown command")
os.Exit(1)
}
}
Items to remember
Independent Help Menus: If you run
program fetch -h, it will only show the help for the fetch command. If you runprogram send -h, it shows the send options.flag.ExitOnError: This tells the program to automatically print the usage and exit if the user provides an invalid flag or types-h. This is the practice that I do all the time.When using
NewFlagSet, the globalflag.Parse()(from my previous example) is not used. Each sub-command has its own.Parse()method. If you call the globalflag.Parse()at the top of yourmain, it will consume your subcommand names as if they were positional arguments, and yourswitchstatement might fail.
Command Handlers
As your CLI tool grows, putting everything in main.go creates a “giant switch statement” that becomes hard to maintain. To keep things clean, the standard practice is to move each command’s logic into its own function (or even its own file).
Here is the professional way to structure subcommands using a functional approach.
Instead of defining flags in main, you define a function for each command that sets up its own flags, parses them, and executes the logic.
package main
import (
"flag"
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: mytool <command> [arguments]")
fmt.Println("Commands: upload, download")
os.Exit(1)
}
// Route to the correct function
switch os.Args[1] {
case "upload":
handleUpload(os.Args[2:])
case "download":
handleDownload(os.Args[2:])
default:
fmt.Printf("Unknown command: %s\n", os.Args[1])
os.Exit(1)
}
}
// --- Command Handlers ---
func handleUpload(args []string) {
fs := flag.NewFlagSet("upload", flag.ExitOnError)
file := fs.String("file", "", "Path to file")
fs.Parse(args)
if *file == "" {
fmt.Println("Error: --file is required")
fs.Usage()
os.Exit(1)
}
fmt.Printf("Uploading %s...\n", *file)
}
func handleDownload(args []string) {
fs := flag.NewFlagSet("download", flag.ExitOnError)
id := fs.Int("id", 0, "ID of the item to download")
fs.Parse(args)
fmt.Printf("Downloading item %d...\n", *id)
}
Benefits of this Structure
Scope Isolation: The flags for
uploaddon’t exist in thedownloadfunction. This prevents “variable name collisions.”File Separation: You can easily move
handleUploadinto a file namedupload.goandhandleDownloadintodownload.go. As long as they are in the samepackage main, it works perfectly.Validation: You can perform custom validation (like checking if a string is empty) immediately after parsing within the function.
When to move beyond the flag package?
While the standard flag library is powerful, it has some limitations:
It doesn’t support “Short” vs “Long” flags (e.g.,
-vand--verboseas the same thing).It doesn’t automatically handle nested subcommands (e.g.,
git remote add origin).
If you find yourself needing those features, you can use libraries like Kong or Cobra.
For comments, please send me 📧 an email.