Go Flags: Beyond the Basics
Go's flag package helps you build command-line apps with argument parsing. Basic usage: flag.String(), flag.Int(), flag.Bool() for simple flags.
Join the DZone community and get the full member experience.
Join For FreeThe flag
package is one of Go's most powerful standard library tools for building command-line applications. Understanding flags is essential whether you're creating a simple CLI tool or a complex application. Let's look into what makes this package so versatile.
Basic Flag Concepts
Let's start with a simple example that demonstrates the core concepts:
package main
import (
"flag"
"fmt"
)
func main() {
// Basic flag definitions
name := flag.String("name", "guest", "your name")
age := flag.Int("age", 0, "your age")
isVerbose := flag.Bool("verbose", false, "enable verbose output")
// Parse the flags
flag.Parse()
// Use the flags
fmt.Printf("Hello %s (age: %d)!\n", *name, *age)
if *isVerbose {
fmt.Println("Verbose mode enabled")
}
}
Advanced Usage Patterns
Custom Flag Types
Sometimes, you need flags that aren't just simple types. Here's how to create a custom flag type:
type IntervalFlag struct {
Duration time.Duration
}
func (i *IntervalFlag) String() string {
return i.Duration.String()
}
func (i *IntervalFlag) Set(value string) error {
duration, err := time.ParseDuration(value)
if err != nil {
return err
}
i.Duration = duration
return nil
}
func main() {
interval := IntervalFlag{Duration: time.Second}
flag.Var(&interval, "interval", "interval duration (e.g., 10s, 1m)")
flag.Parse()
}
Using Flag Sets
FlagSets are perfect for organizing flags in larger applications:
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Create flag sets for different commands
serverCmd := flag.NewFlagSet("server", flag.ExitOnError)
serverPort := serverCmd.Int("port", 8080, "server port")
serverHost := serverCmd.String("host", "localhost", "server host")
clientCmd := flag.NewFlagSet("client", flag.ExitOnError)
clientTimeout := clientCmd.Duration("timeout", 30*time.Second, "client timeout")
clientAddr := clientCmd.String("addr", "localhost:8080", "server address")
if len(os.Args) < 2 {
fmt.Println("expected 'server' or 'client' subcommand")
os.Exit(1)
}
switch os.Args[1] {
case "server":
serverCmd.Parse(os.Args[2:])
runServer(*serverHost, *serverPort)
case "client":
clientCmd.Parse(os.Args[2:])
runClient(*clientAddr, *clientTimeout)
default:
fmt.Printf("unknown subcommand: %s\n", os.Args[1])
os.Exit(1)
}
}
func runServer(host string, port int) {
fmt.Printf("Running server on %s:%d\n", host, port)
}
func runClient(addr string, timeout time.Duration) {
fmt.Printf("Connecting to %s with timeout %v\n", addr, timeout)
}
Real-World Example: Configuration Manager
Here's a practical example of using flags to create a configuration management tool:
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
)
type Config struct {
ConfigPath string
LogLevel string
Port int
Debug bool
DataDir string
}
func main() {
config := parseFlags()
if err := run(config); err != nil {
log.Fatal(err)
}
}
func parseFlags() *Config {
config := &Config{}
// Define flags with environment variable fallbacks
flag.StringVar(&config.ConfigPath, "config",
getEnvOrDefault("APP_CONFIG", "config.yaml"),
"path to config file")
flag.StringVar(&config.LogLevel, "log-level",
getEnvOrDefault("APP_LOG_LEVEL", "info"),
"logging level (debug, info, warn, error)")
flag.IntVar(&config.Port, "port",
getEnvIntOrDefault("APP_PORT", 8080),
"server port number")
flag.BoolVar(&config.Debug, "debug",
getEnvBoolOrDefault("APP_DEBUG", false),
"enable debug mode")
flag.StringVar(&config.DataDir, "data-dir",
getEnvOrDefault("APP_DATA_DIR", "./data"),
"directory for data storage")
// Custom usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nEnvironment variables:\n")
fmt.Fprintf(os.Stderr, " APP_CONFIG Path to config file\n")
fmt.Fprintf(os.Stderr, " APP_LOG_LEVEL Logging level\n")
fmt.Fprintf(os.Stderr, " APP_PORT Server port\n")
fmt.Fprintf(os.Stderr, " APP_DEBUG Debug mode\n")
fmt.Fprintf(os.Stderr, " APP_DATA_DIR Data directory\n")
}
flag.Parse()
// Validate configurations
if err := validateConfig(config); err != nil {
log.Fatalf("Invalid configuration: %v", err)
}
return config
}
func validateConfig(config *Config) error {
// Check if config file exists
if _, err := os.Stat(config.ConfigPath); err != nil {
return fmt.Errorf("config file not found: %s", config.ConfigPath)
}
// Validate log level
switch config.LogLevel {
case "debug", "info", "warn", "error":
// Valid log levels
default:
return fmt.Errorf("invalid log level: %s", config.LogLevel)
}
// Validate port range
if config.Port < 1 || config.Port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
// Ensure data directory exists or create it
if err := os.MkdirAll(config.DataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %v", err)
}
return nil
}
// Helper functions for environment variables
func getEnvOrDefault(key, defaultValue string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return defaultValue
}
func getEnvIntOrDefault(key string, defaultValue int) int {
if value, exists := os.LookupEnv(key); exists {
if parsed, err := strconv.Atoi(value); err == nil {
return parsed
}
}
return defaultValue
}
func getEnvBoolOrDefault(key string, defaultValue bool) bool {
if value, exists := os.LookupEnv(key); exists {
if parsed, err := strconv.ParseBool(value); err == nil {
return parsed
}
}
return defaultValue
}
func run(config *Config) error {
log.Printf("Starting application with configuration:")
log.Printf(" Config Path: %s", config.ConfigPath)
log.Printf(" Log Level: %s", config.LogLevel)
log.Printf(" Port: %d", config.Port)
log.Printf(" Debug Mode: %v", config.Debug)
log.Printf(" Data Directory: %s", config.DataDir)
// Application logic here
return nil
}
Best Practices and Tips
1. Default Values
Always provide sensible default values for your flags.
port := flag.Int("port", 8080, "port number (default 8080)")
2. Clear Descriptions
Write clear, concise descriptions for each flag.
flag.StringVar(&config, "config", "config.yaml", "path to configuration file")
3. Environment Variable Support
Consider supporting both flags and environment variables.
flag.StringVar(&logLevel, "log-level", os.Getenv("LOG_LEVEL"), "logging level")
4. Validation
Always validate flag values after parsing.
if *port < 1 || *port > 65535 {
log.Fatal("Port must be between 1 and 65535")
}
5. Custom Usage
Provide clear usage information
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
Advanced Patterns
Combining With cobra
While the flag
package is powerful, you might want to combine it with more robust CLI frameworks like cobra
:
package main
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func main() {
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application description",
}
// Add flags that can also be set via environment variables
rootCmd.PersistentFlags().String("config", "", "config file (default is $HOME/.myapp.yaml)")
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
Common Pitfalls to Avoid
- Not calling flag.Parse(). Always call
flag.Parse()
after defining your flags. - Ignoring errors. Handle flag parsing errors appropriately.
- Using global flags. Prefer using FlagSets for better organization in larger applications.
- Missing documentation. Always provide clear documentation for all flags.
Conclusion
The flag
package is a powerful tool for building command-line applications in Go. You can create robust and user-friendly CLI applications by understanding its advanced features and following best practices. Remember to validate inputs, provide clear documentation, and consider combining with other packages for more complex applications.
Good luck with your CLI development!
Opinions expressed by DZone contributors are their own.
Comments