If you have run kubectl get pods --namespace web, you have used Cobra. It is the dominant CLI framework in the Go ecosystem and the structure underlying virtually every cloud-native command-line tool.
The Cobra Mental Model
A CLI is a tree of commands:
myapp # root
├── version
├── deploy
│ ├── --env
│ └── --image
├── status
└── config
├── get <key>
└── set <key> <value>
Each node is a *cobra.Command with its own flags, args, and run function. Children attach to parents with AddCommand.
Minimal Example
// cmd/root.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var (
verbose bool
rootCmd = &cobra.Command{
Use: "myapp",
Short: "Manage acme deployments",
Long: "myapp is the command-line interface for the acme platform.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if verbose { fmt.Println("verbose mode") }
},
}
)
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
}
func Execute() error {
return rootCmd.Execute()
}
// cmd/deploy.go
var (
env string
image string
deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy an application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
return deploy(name, env, image)
},
}
)
func init() {
deployCmd.Flags().StringVar(&env, "env", "dev", "target environment")
deployCmd.Flags().StringVar(&image, "image", "", "container image (required)")
deployCmd.MarkFlagRequired("image")
rootCmd.AddCommand(deployCmd)
}
// cmd/myapp/main.go
package main
import (
"os"
"github.com/acme/myapp/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
Run:
myapp deploy checkout --env prod --image ghcr.io/acme/checkout:v2.7.0
myapp deploy --help
Cobra generates the help text, completion scripts, and man pages from the same metadata. myapp completion bash outputs a bash completion script for free.
Flags vs Persistent Flags
cmd.Flags()— apply only to that commandcmd.PersistentFlags()— apply to this command and all its children
--verbose on the root is persistent; --image on deploy is not.
Argument Validation
| Helper | Behaviour |
|---|---|
cobra.NoArgs | No positional args |
cobra.ExactArgs(n) | Exactly n |
cobra.MinimumNArgs(n) | At least n |
cobra.MaximumNArgs(n) | At most n |
cobra.RangeArgs(min, max) | Between min and max |
Custom PositionalArgs function | Anything you like |
Run vs RunE
Run— returns nothing; you handle errors yourselfRunE— returns an error; Cobra prints it and exits non-zero
Use RunE. Then in main: os.Exit(1) on error.
Viper for Configuration
Viper is Cobra's sibling. It layers configuration sources with a clear precedence:
- Explicit
--flagon the command line - Environment variable (e.g.,
MYAPP_ENV=prod) - Config file (
~/.myapp.yaml,./myapp.yaml) - Remote config (etcd, Consul) — rarely used in CLIs
- Default value from Viper
import "github.com/spf13/viper"
func init() {
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv() // MYAPP_ENV -> "env"
viper.SetDefault("env", "dev")
cobra.OnInitialize(func() {
viper.SetConfigName(".myapp")
viper.AddConfigPath("$HOME")
viper.AddConfigPath(".")
_ = viper.ReadInConfig()
})
deployCmd.Flags().String("env", "", "target environment")
viper.BindPFlag("env", deployCmd.Flags().Lookup("env"))
}
// Read:
env := viper.GetString("env")
The kubectl Look-and-Feel
Two patterns kubectl popularised that are now community standard:
Output formatting flag
myapp get deployments -o json
myapp get deployments -o yaml
myapp get deployments -o wide
Implement once at the root with a persistent --output / -o flag and a small dispatcher (encoding/json, sigs.k8s.io/yaml, tabular for wide).
Resource verbs
myapp create deployment my-app --image ...
myapp get deployments
myapp describe deployment my-app
myapp delete deployment my-app
Commands organised by verb-then-noun. Kubectl made it standard; users expect it.
Interactive Output
- Colour — fatih/color handles ANSI escape codes and respects
NO_COLORautomatically - Spinners and progress — bubbletea or schollz/progressbar
- Tables —
text/tabwriterin the stdlib for simple cases; go-pretty for fancier - Prompts — survey for interactive Q&A
Always check isatty (mattn/go-isatty) — if stdout is a pipe, emit plain output.
Distribution with goreleaser
goreleaser is the standard release tool. One config file ships:
- Cross-compiled binaries for Linux / macOS / Windows on amd64 / arm64
- SHA256 checksums and signed archives
- GitHub Releases with auto-generated changelogs
- Homebrew tap, Scoop bucket, deb/rpm packages, Docker images, SBOM
- SLSA provenance, cosign signing
A minimal .goreleaser.yml:
builds:
- main: ./cmd/myapp
binary: myapp
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
env: [CGO_ENABLED=0]
ldflags:
- -s -w -X main.version={{.Version}}
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip
brews:
- tap:
owner: acme
name: homebrew-tap
Triggered from a tag push in GitHub Actions, the entire release is hands-off.
Real-World Reference
Read the Cobra source of kubectl, helm, or gh. The patterns are conventional and worth copying — Cobra, Viper, goreleaser is the modern Go CLI stack.