Building CLI Apps in Go with Cobra & Viper
CLI development in Go with Cobra and Viper frameworks
Command-line interface (CLI) applications are essential tools for developers, system administrators, and DevOps professionals. Two Go libraries have become the de facto standard for CLI development in Go: Cobra for command structure and Viper for configuration management.
Go has emerged as an excellent language for building CLI tools due to its performance, simple deployment, and cross-platform support.

Why Choose Go for CLI Applications
Go offers compelling advantages for CLI development:
- Single Binary Distribution: No runtime dependencies or package managers required
- Fast Execution: Native compilation provides excellent performance
- Cross-Platform Support: Easy compilation for Linux, macOS, Windows, and more
- Strong Standard Library: Rich tooling for file I/O, networking, and text processing
- Concurrency: Built-in goroutines for parallel operations
- Static Typing: Catch errors at compile time
Popular CLI tools built with Go include Docker, Kubernetes (kubectl), Hugo, Terraform, and GitHub CLI. If you’re new to Go or need a quick reference, check out our Go Cheat Sheet for essential Go syntax and patterns.
Introduction to Cobra
Cobra is a library that provides a simple interface for creating powerful modern CLI applications. Created by Steve Francia (spf13), the same author behind Hugo and Viper, Cobra is used in many of the most popular Go projects.
Key Cobra Features
Command Structure: Cobra implements the command pattern, allowing you to create applications with commands and subcommands (like git commit or docker run).
Flag Handling: Both local and persistent flags with automatic parsing and type conversion.
Automatic Help: Generates help text and usage information automatically.
Intelligent Suggestions: Provides suggestions when users make typos (“Did you mean ‘status’?”).
Shell Completions: Generate completion scripts for bash, zsh, fish, and PowerShell.
Flexible Output: Works seamlessly with custom formatters and output styles.
Introduction to Viper
Viper is a complete configuration solution for Go applications, designed to work seamlessly with Cobra. It handles configuration from multiple sources with a clear precedence order.
Key Viper Features
Multiple Configuration Sources:
- Config files (JSON, YAML, TOML, HCL, INI, envfile, Java properties)
- Environment variables
- Command-line flags
- Remote config systems (etcd, Consul)
- Default values
Configuration Priority Order:
- Explicit calls to Set
- Command-line flags
- Environment variables
- Configuration file
- Key/value store
- Default values
Live Watching: Monitor configuration files and reload automatically when changes occur.
Type Conversion: Automatic conversion to various Go types (string, int, bool, duration, etc.).
Getting Started: Installation
First, initialize a new Go module and install both libraries:
go mod init myapp
go get -u github.com/spf13/cobra@latest
go get -u github.com/spf13/viper@latest
Optionally, install the Cobra CLI generator for scaffolding:
go install github.com/spf13/cobra-cli@latest
Building Your First CLI Application
Let’s build a practical example: a task management CLI tool with configuration support.
Project Structure
mytasks/
├── cmd/
│ ├── root.go
│ ├── add.go
│ ├── list.go
│ └── complete.go
├── config/
│ └── config.go
├── main.go
└── config.yaml
Main Entry Point
// main.go
package main
import "mytasks/cmd"
func main() {
cmd.Execute()
}
Root Command with Viper Integration
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "mytasks",
Short: "A simple task management CLI",
Long: `MyTasks is a CLI task manager that helps you organize
your daily tasks with ease. Built with Cobra and Viper.`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
"config file (default is $HOME/.mytasks.yaml)")
rootCmd.PersistentFlags().String("db", "",
"database file location")
viper.BindPFlag("database", rootCmd.PersistentFlags().Lookup("db"))
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName(".mytasks")
}
viper.SetEnvPrefix("MYTASKS")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
Adding Subcommands
// cmd/add.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var priority string
var addCmd = &cobra.Command{
Use: "add [task description]",
Short: "Add a new task",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
task := args[0]
db := viper.GetString("database")
fmt.Printf("Adding task: %s\n", task)
fmt.Printf("Priority: %s\n", priority)
fmt.Printf("Database: %s\n", db)
// Here you would implement actual task storage
},
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().StringVarP(&priority, "priority", "p", "medium",
"Task priority (low, medium, high)")
}
List Command
// cmd/list.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var showCompleted bool
var listCmd = &cobra.Command{
Use: "list",
Short: "List all tasks",
Run: func(cmd *cobra.Command, args []string) {
db := viper.GetString("database")
fmt.Printf("Listing tasks from: %s\n", db)
fmt.Printf("Show completed: %v\n", showCompleted)
// Here you would implement actual task listing
},
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false,
"Show completed tasks")
}
Implementing Data Persistence
For a production task management CLI, you’ll need to implement actual data storage. While we’re using a simple database path configuration here, you have several options for persisting data:
- SQLite: Lightweight, serverless database perfect for CLI tools
- PostgreSQL/MySQL: Full-featured databases for more complex applications
- JSON/YAML files: Simple file-based storage for lightweight needs
- Embedded databases: BoltDB, BadgerDB for key-value storage
If you’re working with relational databases like PostgreSQL, you’ll want to use an ORM or query builder. For a comprehensive comparison of Go database libraries, see our guide on Comparing Go ORMs for PostgreSQL: GORM vs Ent vs Bun vs sqlc.
Advanced Configuration with Viper
Configuration File Example
# .mytasks.yaml
database: ~/.mytasks.db
format: table
colors: true
notifications:
enabled: true
sound: true
priorities:
high: red
medium: yellow
low: green
Reading Nested Configuration
func getNotificationSettings() {
enabled := viper.GetBool("notifications.enabled")
sound := viper.GetBool("notifications.sound")
fmt.Printf("Notifications enabled: %v\n", enabled)
fmt.Printf("Sound enabled: %v\n", sound)
}
func getPriorityColors() map[string]string {
return viper.GetStringMapString("priorities")
}
Environment Variables
Viper automatically reads environment variables with the configured prefix:
export MYTASKS_DATABASE=/tmp/tasks.db
export MYTASKS_NOTIFICATIONS_ENABLED=false
mytasks list
Live Configuration Reload
func watchConfig() {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
// Reload configuration-dependent components
})
}
Best Practices
1. Use Cobra Generator for Consistency
cobra-cli init
cobra-cli add serve
cobra-cli add create
2. Organize Commands in Separate Files
Keep each command in its own file under the cmd/ directory for maintainability.
3. Leverage Persistent Flags
Use persistent flags for options that apply to all subcommands:
rootCmd.PersistentFlags().StringP("output", "o", "text",
"Output format (text, json, yaml)")
4. Implement Proper Error Handling
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
RunE: func(cmd *cobra.Command, args []string) error {
if err := doSomething(); err != nil {
return fmt.Errorf("failed to do something: %w", err)
}
return nil
},
}
5. Provide Meaningful Help Text
var cmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application to specified environment",
Long: `Deploy builds and deploys your application to the
specified environment. Supported environments are:
- development (dev)
- staging
- production (prod)`,
Example: ` myapp deploy staging
myapp deploy production --version=1.2.3`,
}
6. Set Sensible Defaults
func init() {
viper.SetDefault("port", 8080)
viper.SetDefault("timeout", "30s")
viper.SetDefault("retries", 3)
}
7. Validate Configuration
func validateConfig() error {
port := viper.GetInt("port")
if port < 1024 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
return nil
}
Testing CLI Applications
Testing Commands
// cmd/root_test.go
package cmd
import (
"bytes"
"testing"
)
func TestRootCommand(t *testing.T) {
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"--help"})
if err := cmd.Execute(); err != nil {
t.Fatalf("command execution failed: %v", err)
}
}
Testing with Different Configurations
func TestWithConfig(t *testing.T) {
viper.Set("database", "/tmp/test.db")
viper.Set("debug", true)
// Run your tests
viper.Reset() // Clean up
}
Generating Shell Completions
Cobra can generate completion scripts for various shells:
// cmd/completion.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
$ source <(mytasks completion bash)
Zsh:
$ source <(mytasks completion zsh)
Fish:
$ mytasks completion fish | source
PowerShell:
PS> mytasks completion powershell | Out-String | Invoke-Expression
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}
Building and Distribution
Build for Multiple Platforms
# Linux
GOOS=linux GOARCH=amd64 go build -o mytasks-linux-amd64
# macOS
GOOS=darwin GOARCH=amd64 go build -o mytasks-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o mytasks-darwin-arm64
# Windows
GOOS=windows GOARCH=amd64 go build -o mytasks-windows-amd64.exe
Reduce Binary Size
go build -ldflags="-s -w" -o mytasks
Add Version Information
// cmd/version.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var (
Version = "dev"
Commit = "none"
BuildTime = "unknown"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s\n", Version)
fmt.Printf("Commit: %s\n", Commit)
fmt.Printf("Built: %s\n", BuildTime)
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
Build with version information:
go build -ldflags="-X 'mytasks/cmd.Version=1.0.0' \
-X 'mytasks/cmd.Commit=$(git rev-parse HEAD)' \
-X 'mytasks/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
-o mytasks
Real-World Examples
Popular open-source tools built with Cobra and Viper:
- kubectl: Kubernetes command-line tool
- Hugo: Static site generator
- GitHub CLI (gh): GitHub’s official CLI
- Docker CLI: Container management
- Helm: Kubernetes package manager
- Skaffold: Kubernetes development workflow tool
- Cobra CLI: Self-hosting - Cobra uses itself!
Beyond traditional DevOps tools, Go’s CLI capabilities extend to AI and machine learning applications. If you’re interested in building CLI tools that interact with large language models, check out our guide on Constraining LLMs with Structured Output using Ollama and Go, which shows how to build Go applications that work with AI models.
Useful Resources
- Cobra GitHub Repository
- Viper GitHub Repository
- Cobra User Guide
- Viper Documentation
- Go CLI Applications Best Practices
Conclusion
Cobra and Viper together provide a powerful foundation for building professional CLI applications in Go. Cobra handles the command structure, flag parsing, and help generation, while Viper manages configuration from multiple sources with intelligent precedence.
This combination allows you to create CLI tools that are:
- Easy to use with intuitive commands and automatic help
- Flexible with multiple configuration sources
- Professional with shell completions and proper error handling
- Maintainable with clean code organization
- Portable across different platforms
Whether you’re building developer tools, system utilities, or DevOps automation, Cobra and Viper provide the solid foundation you need to create CLI applications that users will love.