BLOG POSTS
How to Use the flag Package in Go

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.

Leave a reply

Your email address will not be published. Required fields are marked