Recommended Free Tools
A Go command-line tool with several subcommands and layered configuration works best when Cobra owns the command tree and flags, Viper merges the configuration sources, and the rest of your application receives one typed configuration struct instead of reading Viper directly. Viper’s documented precedence runs from explicit Set calls, through bound flags and environment variables, to config files, key/value stores and finally defaults. The steps below build that layout in a small example tool called mytool.
Before you start, note that the code is illustrative rather than pinned to a release. Add Cobra and Viper to your module with a specific version recorded in go.mod, and check the current Cobra and Viper documentation for API changes. The official project documentation was accessed on 7 October 2026, and repositories change.
As an Amazon Associate I earn from qualifying purchases.
Set up the project and generate the command skeleton
Create a module and fetch the two libraries. Cobra’s README describes an optional generator, cobra-cli, that writes the boilerplate for you. Install it at a tagged version of your choice and run the following from an empty directory.
-
Initialise the module:
go mod init example.com/mytool.#1 Best Overall
-
Fetch the libraries at the versions you want to pin:
go get github.com/spf13/cobraandgo get github.com/spf13/viper, then review the resulting entries ingo.mod. -
Generate the root command:
cobra-cli init --pkg-name example.com/mytool. -
Generate a subcommand:
cobra-cli add serve.
The layout looks like this:
mytool/
├── go.mod
├── main.go
└── cmd/
├── root.go
└── serve.go
The Cobra guide presents cmd/ with a main.go that only calls into the command package as a common convention, not a requirement. Keep main.go that small so the command tree is testable.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Local and persistent flags
Cobra has two kinds of flag, and the choice determines which commands can see them.
Local flags
A local flag belongs to one command only. Define it on serve with serveCmd.Flags(), and mytool serve --port 9000 works while mytool --port 9000 does not. A child command does not parse a parent’s local flags by default; the Cobra guide documents TraverseChildren for the case where you want that behaviour.
Persistent flags
A persistent flag is defined on a command and inherited by its descendants. Settings that every subcommand needs, such as the config file path and log level, belong on the root command’s PersistentFlags().
Cobra also supports constraints between flags, including required flags, flags that must be given together and mutually exclusive flags. Use these functions when a combination of options is invalid, instead of checking the combination by hand inside RunE.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Where configuration comes from, and in what order
Viper merges values from several sources and returns the highest-ranked one. The table lists the Viper README’s precedence, from highest to lowest, with what each source means for a CLI.
| Rank | Source | How it is set or found | Changes per invocation? | Typical use in a CLI |
|---|---|---|---|---|
| 1 | Explicit Set call |
viper.Set("log.level", "debug") in code |
Only when code runs it | Tests and programmatic overrides |
| 2 | Bound flag | Bound with BindPFlag or BindPFlags |
Yes, when the user passes the flag | One-off overrides typed at the terminal |
| 3 | Environment variable | Read on each access, using the prefix and key mapping you configure | Yes, per shell or process | CI jobs, containers, secrets supplied by a platform |
| 4 | Config file | An explicit path from --config, or a search name in configured directories |
No, it is read once at startup | Shared, persistent settings for a user or project |
| 5 | Key/value store | External store supported by Viper | Depends on the store | Not used in this tutorial |
| 6 | Default | viper.SetDefault |
No | Fallback values when nothing else is set |
Viper keys are case-insensitive, while environment variable names are case-sensitive. Use lowercase, dot-separated keys such as log.level and map them to environment names explicitly.
Keep flag defaults out of the picture if a config file should be able to win. A flag’s own default is the value Cobra uses when the flag is not passed, and it sits at the flag layer, above the config file. Put the fallback in viper.SetDefault instead, so it sits at the bottom of the stack.
Wire Cobra to Viper
The root command registers a single initialisation function with cobra.OnInitialize. Cobra runs it before a command executes, after flags are parsed, so the config file and environment are ready when your commands read them. Replace the generated cmd/root.go with the following.
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "Example tool with layered configuration",
Long: "mytool runs commands configured by flags, environment variables, or a config file.",
SilenceUsage: true,
SilenceErrors: true,
}
// Execute runs the root command and prints any returned error once.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path")
rootCmd.PersistentFlags().String("log-level", "info", "log level: debug, info, warn, error")
if err := viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level")); err != nil {
panic(err)
}
}
func initConfig() {
viper.SetEnvPrefix("MYTOOL")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigName(".mytool")
}
if err := viper.ReadInConfig(); err != nil {
var notFound viper.ConfigFileNotFoundError
if cfgFile != "" || !errors.As(err, ¬Found) {
cobra.CheckErr(fmt.Errorf("reading config: %w", err))
}
}
}
Three details matter here. The MYTOOL prefix and the replacer turn log.level into MYTOOL_LOG_LEVEL. The config file is searched for as .mytool.yaml in the home directory, following the Cobra guide’s example of a dot-prefixed file in the home directory; this is an example, not a default you must copy. Viper reads a single config file, so the search order is the only thing that decides which file is used.
Config file discovery and the error rule
Treat a missing file and an unreadable or malformed file as different situations. The guide’s approach, which the code above follows, is this:
-
If the user passes
--config, the file was requested explicitly. Any read error, including a missing file, is fatal. -
If the file is found by search, a missing file is acceptable and the tool runs on flags, environment and defaults. Any other read error, such as invalid YAML or permission problems, is fatal.
Recommended: Update Every Outdated Driver on Your PC in One Scan - Free →Recommended: Fix Windows Errors and Clear Junk Files in Minutes - Free Scan →Recommended: Crashes or Glitches? A Free Driver Scan Usually Finds the Culprit →Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Swallowing every error from ReadInConfig hides typos in a config file until the value is silently ignored, which is the most common support question for this kind of tool. After a successful read, viper.ConfigFileUsed() returns the path that was loaded, which is useful for a --verbose message or for a config subcommand that reports where settings came from.
Environment variables and key mapping
Environment variables are read every time they are accessed rather than cached, so changes take effect without a restart of the library state, though the process itself still has to be started with the new value. An empty variable is treated as unset unless you call viper.AllowEmptyEnv(true). Name each mapping on purpose:
| Config key | Environment variable | Config file form | How the name is produced |
|---|---|---|---|
log.level |
MYTOOL_LOG_LEVEL |
log: then level: nested beneath it |
Prefix, then the replacer turns dots into underscores |
server.port |
MYTOOL_SERVER_PORT |
server: then port: nested beneath it |
Same rule as above |
api_token |
MYTOOL_API_TOKEN |
api_token: at the top level |
Explicit BindEnv call, shown below |
The unmarshalling gotcha
The spf13 Go skills guide, an implementation guide from the project skills repository rather than Cobra itself, warns that AutomaticEnv combined with viper.Unmarshal can miss a key that exists only in the environment. Viper builds the struct from the keys it knows about. A key that is never defined, bound or defaulted may be invisible to Unmarshal, even when the environment variable is set. Register such keys explicitly with BindEnv (or with a default) before unmarshalling.
Rank #4
Passing configuration to application code
The same skills guide recommends unmarshalling once into a typed struct and passing that struct down, instead of letting every package call Viper’s global getters. The benefit is that business logic can be tested with an ordinary struct value. Define the struct in cmd/config.go.
package cmd
type Config struct {
Log LogConfig `mapstructure:"log"`
Server ServerConfig `mapstructure:"server"`
APIToken string `mapstructure:"api_token"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
}
Now add the serve command, which owns its port flag and the explicit token binding.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var serveCmd = &cobra.Command{
Use: "serve [flags]",
Short: "Start the HTTP server",
Long: "serve starts the HTTP server. Flags override environment variables, which override the config file.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
var c Config
if err := viper.Unmarshal(&c); err != nil {
return fmt.Errorf("decoding configuration: %w", err)
}
return runServer(c)
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().Int("port", 8080, "port to listen on")
if err := viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port")); err != nil {
panic(err)
}
if err := viper.BindEnv("api_token", "MYTOOL_API_TOKEN"); err != nil {
panic(err)
}
}
// runServer is application code. It receives a Config and never calls Viper.
func runServer(c Config) error {
fmt.Printf("listening on port %d at log level %sn", c.Server.Port, c.Log.Level)
return nil
}
Binding server.port to the flag means the flag’s value takes precedence over MYTOOL_SERVER_PORT and over server.port in the config file. Binding does not copy values into the Go variable behind a flag, so read merged values through the typed struct, not through the variable you passed to a flag function.
Errors and exit behaviour
Return errors from RunE rather than calling os.Exit inside command logic, so the top-level Execute decides how to report them. Setting SilenceUsage to true keeps a runtime failure from printing the full help text after the error, and SilenceErrors stops Cobra from printing the error a second time because Execute already prints it. Wrap errors with %w so callers can still inspect them with errors.Is or errors.As.
Help text, documentation and shell completion
Cobra generates help for every command and a help command for applications with subcommands. The text you write in Use, Short, Long and each flag’s usage string becomes the interface users read, so write those fields as complete sentences. The Cobra README puts the principle this way: “The best applications read like sentences when used, and as a result, users intuitively know how to interact with them.”
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteThe Cobra guide also describes generating command documentation and shell completion scripts for Bash, Zsh, Fish and PowerShell. For example, mytool completion bash prints a Bash script that you then load according to your shell’s configuration. Check mytool completion --help in your version, since the subcommand names come from the Cobra release you pin.
Best Value
Troubleshooting checklist
-
A config value is ignored. Check the precedence table. A flag (passed or defaulted at the flag layer) or an environment variable overrides the file, and a
Setcall overrides everything. -
An environment variable is set but does not reach the struct. Confirm the variable name matches the mapping. Then confirm the key is known to Viper through
BindEnv,SetDefaultor a bound flag beforeUnmarshalruns. -
An empty variable is not used. By default an empty value counts as unset. Call
viper.AllowEmptyEnv(true)only if an empty string is meaningful.Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchSpecial offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy. -
The config file is not used. Print
viper.ConfigFileUsed()after reading. An empty result means no file was loaded, which is expected when a search finds nothing. -
The command fails with a config error on startup. Read the error. A parse error or permission problem is reported for every path, and a missing explicit
--configfile is also reported as an error. -
A dotted key does not map to an environment variable. Confirm
SetEnvKeyReplacerruns before the first read, and that the prefix inSetEnvPrefixmatches what users type. -
Code reads a flag variable and gets an unexpected value. Read merged values through Viper or the typed struct, not the variable that was passed to
StringVar.Free tools Windows power users keep installed
One-click scans. No signup required.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Keep secrets such as api_token out of files you commit to version control. Supply them through environment variables or a secret manager your platform provides, and keep the config file for settings that are safe to share.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




