
How to Use the flag Package in Go
The flag package in Go is one of those deceptively simple tools that becomes absolutely essential once you start building command-line applications at scale. Whether you’re creating microservices, system utilities, or deployment scripts, knowing how to properly handle command-line arguments can make the difference between a tool that’s a joy to use and one that makes everyone groan. In this guide, we’ll dive deep into Go’s built-in flag package, explore advanced patterns, compare it with alternatives, and walk through real-world implementations that you can actually use in production.
How the Flag Package Works
Go’s flag package follows a straightforward approach to command-line argument parsing. Under the hood, it builds a registry of expected flags during program initialization, then parses os.Args when you call flag.Parse(). The package supports three main types: string, int, and bool flags, with each type having multiple definition methods.
The parsing logic is surprisingly robust. For boolean flags, it accepts various forms like -debug
, -debug=true
, or -debug=false
. String and integer flags require values, either as -port=8080
or -port 8080
. The package also automatically generates help text and handles the -h
and --help
flags.
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Different ways to define flags
var port = flag.Int("port", 8080, "Port to listen on")
var host = flag.String("host", "localhost", "Host to bind to")
debug := flag.Bool("debug", false, "Enable debug mode")
// Parse command line arguments
flag.Parse()
fmt.Printf("Server will run on %s:%d, debug: %t\n", *host, *port, *debug)
// Access remaining non-flag arguments
args := flag.Args()
if len(args) > 0 {
fmt.Printf("Additional arguments: %v\n", args)
}
}
Step-by-Step Implementation Guide
Let’s build a practical example – a file processing utility that demonstrates most flag package features. This will give you a solid foundation for your own CLI tools.
Basic Setup
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
)
type Config struct {
InputDir string
OutputDir string
Pattern string
Workers int
Verbose bool
Timeout time.Duration
DryRun bool
}
func main() {
config := parseFlags()
if config.Verbose {
log.Printf("Starting file processor with config: %+v", config)
}
// Your application logic here
processFiles(config)
}
func parseFlags() *Config {
config := &Config{}
flag.StringVar(&config.InputDir, "input", "./input", "Input directory path")
flag.StringVar(&config.OutputDir, "output", "./output", "Output directory path")
flag.StringVar(&config.Pattern, "pattern", "*.txt", "File pattern to match")
flag.IntVar(&config.Workers, "workers", 4, "Number of worker goroutines")
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose logging")
flag.DurationVar(&config.Timeout, "timeout", 30*time.Second, "Processing timeout")
flag.BoolVar(&config.DryRun, "dry-run", false, "Show what would be done without executing")
flag.Parse()
return config
}
Advanced Flag Handling
For more complex scenarios, you might need custom flag types or validation. Here’s how to implement custom flag values:
// Custom flag type for comma-separated strings
type StringSlice []string
func (s *StringSlice) String() string {
return strings.Join(*s, ",")
}
func (s *StringSlice) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}
// Usage in main function
func parseAdvancedFlags() *Config {
config := &Config{}
var extensions StringSlice
flag.Var(&extensions, "ext", "Comma-separated list of file extensions")
// Custom validation
flag.Parse()
// Post-parse validation
if config.Workers < 1 || config.Workers > 100 {
log.Fatal("Workers must be between 1 and 100")
}
if !dirExists(config.InputDir) {
log.Fatalf("Input directory does not exist: %s", config.InputDir)
}
return config
}
func dirExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}
Real-World Examples and Use Cases
Here are some practical scenarios where the flag package shines, especially when building tools for VPS or dedicated server management:
Database Migration Tool
package main
import (
"flag"
"fmt"
"log"
"os"
)
func main() {
var (
dbHost = flag.String("db-host", "localhost", "Database host")
dbPort = flag.Int("db-port", 5432, "Database port")
dbName = flag.String("db-name", "", "Database name (required)")
dbUser = flag.String("db-user", "", "Database user (required)")
dbPass = flag.String("db-pass", "", "Database password")
migrateUp = flag.Bool("up", false, "Run migrations up")
migrateDown = flag.Bool("down", false, "Run migrations down")
steps = flag.Int("steps", 0, "Number of migration steps (0 = all)")
configFile = flag.String("config", "", "Configuration file path")
)
flag.Parse()
// Validation
if *dbName == "" || *dbUser == "" {
fmt.Fprintf(os.Stderr, "Error: db-name and db-user are required\n\n")
flag.Usage()
os.Exit(1)
}
if !*migrateUp && !*migrateDown {
log.Fatal("Must specify either -up or -down")
}
// Build connection string
connStr := fmt.Sprintf("host=%s port=%d dbname=%s user=%s",
*dbHost, *dbPort, *dbName, *dbUser)
if *dbPass != "" {
connStr += fmt.Sprintf(" password=%s", *dbPass)
}
fmt.Printf("Connecting to: %s\n", connStr)
// Migration logic here...
}
Log Analysis Tool
package main
import (
"flag"
"fmt"
"time"
)
type LogConfig struct {
Files []string
StartTime time.Time
EndTime time.Time
Pattern string
OutputFormat string
Follow bool
LineNumbers bool
}
func main() {
var (
startTime = flag.String("start", "", "Start time (RFC3339 format)")
endTime = flag.String("end", "", "End time (RFC3339 format)")
pattern = flag.String("pattern", "", "Regex pattern to match")
format = flag.String("format", "text", "Output format: text, json, csv")
follow = flag.Bool("follow", false, "Follow log files (like tail -f)")
lineNums = flag.Bool("line-numbers", false, "Show line numbers")
)
flag.Parse()
config := &LogConfig{
Files: flag.Args(), // Remaining arguments are file paths
Pattern: *pattern,
OutputFormat: *format,
Follow: *follow,
LineNumbers: *lineNums,
}
// Parse time strings
if *startTime != "" {
t, err := time.Parse(time.RFC3339, *startTime)
if err != nil {
log.Fatalf("Invalid start time format: %v", err)
}
config.StartTime = t
}
if len(config.Files) == 0 {
fmt.Fprintf(os.Stderr, "Usage: %s [options] logfile1 logfile2...\n", os.Args[0])
flag.PrintDefaults()
os.Exit(1)
}
analyzeLog(config)
}
Comparisons with Alternatives
While Go’s built-in flag package is solid for basic use cases, several third-party libraries offer additional features. Here’s a detailed comparison:
Library | Pros | Cons | Best For |
---|---|---|---|
flag (built-in) | Zero dependencies, simple API, automatic help generation | Limited customization, no subcommands, basic types only | Simple CLI tools, internal utilities |
cobra | Subcommands, rich help system, shell completion, widely adopted | Heavy dependency, complexity overhead for simple tools | Complex CLI applications, kubectl-style tools |
urfave/cli | Middleware support, subcommands, good balance of features | Less ecosystem support than cobra | Medium complexity applications |
pflag | POSIX-compliant, drop-in replacement for flag, shorthand options | Still no subcommands, additional dependency | When you need POSIX compliance |
Migration Example: Flag to Cobra
If you outgrow the flag package, here’s how a migration might look:
// Before: using flag package
func main() {
var port = flag.Int("port", 8080, "Port to listen on")
var host = flag.String("host", "localhost", "Host to bind to")
flag.Parse()
startServer(*host, *port)
}
// After: using cobra
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
Run: func(cmd *cobra.Command, args []string) {
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetInt("port")
startServer(host, port)
},
}
func init() {
rootCmd.Flags().StringP("host", "h", "localhost", "Host to bind to")
rootCmd.Flags().IntP("port", "p", 8080, "Port to listen on")
}
func main() {
rootCmd.Execute()
}
Best Practices and Common Pitfalls
Do’s and Don’ts
- DO call flag.Parse() before accessing flag values – this is the most common mistake
- DO use meaningful default values and clear descriptions
- DO validate flag values after parsing when business logic requires it
- DON’T use global variables for flags in larger applications – pass config structs instead
- DON’T forget that flag values are pointers when using flag.String(), flag.Int(), etc.
- DO use flag.Var() for more complex flag types
Production-Ready Error Handling
package main
import (
"flag"
"fmt"
"log"
"os"
)
func main() {
// Customize usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, " A production-ready file processor\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " %s -input /data -workers 8\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -pattern '*.log' -verbose\n", os.Args[0])
}
var (
input = flag.String("input", "", "Input directory (required)")
workers = flag.Int("workers", 4, "Number of workers")
verbose = flag.Bool("verbose", false, "Verbose output")
)
flag.Parse()
// Comprehensive validation
if *input == "" {
log.Printf("Error: input directory is required")
flag.Usage()
os.Exit(1)
}
if *workers < 1 || *workers > 32 {
log.Fatal("Workers must be between 1 and 32")
}
// Check for unknown flags by examining flag.Args()
if flag.NArg() > 0 {
log.Printf("Warning: unexpected arguments: %v", flag.Args())
}
if *verbose {
log.Printf("Starting with %d workers, processing %s", *workers, *input)
}
}
Testing CLI Applications
Testing flag-based applications requires some setup, but it’s definitely doable:
package main
import (
"flag"
"os"
"testing"
)
func TestFlagParsing(t *testing.T) {
// Save original command line args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
// Reset flag package state
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
// Set up test arguments
os.Args = []string{"myapp", "-port", "9000", "-host", "example.com"}
// Define flags
port := flag.Int("port", 8080, "Port")
host := flag.String("host", "localhost", "Host")
// Parse
flag.Parse()
// Test
if *port != 9000 {
t.Errorf("Expected port 9000, got %d", *port)
}
if *host != "example.com" {
t.Errorf("Expected host example.com, got %s", *host)
}
}
Environment Variable Integration
A common pattern is to support both flags and environment variables:
package main
import (
"flag"
"os"
"strconv"
)
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvIntOrDefault(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return defaultValue
}
func main() {
// Flags with environment variable fallbacks
port := flag.Int("port",
getEnvIntOrDefault("PORT", 8080),
"Port to listen on (env: PORT)")
host := flag.String("host",
getEnvOrDefault("HOST", "localhost"),
"Host to bind to (env: HOST)")
dbUrl := flag.String("db-url",
getEnvOrDefault("DATABASE_URL", ""),
"Database URL (env: DATABASE_URL)")
flag.Parse()
// Now your application can be configured via flags OR environment variables
startServer(*host, *port, *dbUrl)
}
The flag package might seem basic compared to fancier alternatives, but its simplicity is often exactly what you need. It’s reliable, well-tested, and ships with Go, making it perfect for system utilities, deployment scripts, and internal tools. For more complex applications requiring subcommands or advanced features, consider graduating to cobra or urfave/cli, but don’t underestimate what you can accomplish with Go’s built-in flag package.
For more advanced server management scenarios, check out the official Go documentation at pkg.go.dev/flag and consider how these patterns might integrate with your infrastructure setup.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.