| Tested on | RHEL 10.2 with Go 1.26.5 |
|---|---|
| Package | go 1.26.5github.com/spf13/cobra v1.10.2 |
| Applies to | Any host with Go 1.20+ installed |
| Privilege | Normal user |
| Scope | Hands-on golang cobra tutorial: root command, subcommands, local and persistent flags, --version, and terminal checks with go run. Does not cover Viper config wiring. |
| Related guides | Golang flag examples Golang prompt and interactive CLI Golang Viper Getting started with Go Golang context |
If you searched for golang cobra, go cobra tutorial, or a cobra golang example, you want a real command tree—not a single flag.Parse() call. Cobra is the library behind tools like Kubernetes, Hugo, and GitHub CLI: nested subcommands, POSIX-style flags, generated help, and shell completion. This walkthrough builds a small textcli program with reverse, uppercase, and modify subcommands, wires local and persistent flags, and verifies each path with go run.
What you will build
| Subcommand | Alias | Arguments | Result |
|---|---|---|---|
reverse |
rev |
one string | reversed runes |
uppercase |
upper |
one string | uppercased string |
modify |
mod |
one string + optional -o |
appends a modified or MODIFIED suffix |
A root-level -V / --verbose persistent flag is available on every subcommand; this tutorial wires it into reverse only to show inheritance.
Install Cobra and set up the module
Create the module and add Cobra (pin a release you have tested):
go mod init example.com/textcligo mod init writes go.mod in the current directory. Pull Cobra next:
go get github.com/spf13/cobra@v1.10.2Optional: install the project generator with go install github.com/spf13/cobra-cli@latest. This tutorial wires commands manually; cobra-cli init at the end shows the generated layout.
Lay out a minimal tree—main.go calls cmd.Execute(), and commands live in cmd/root.go:
textcli/
cmd/root.go
helper/helper.go
main.go
go.modCreate the root command
Define the root cobra.Command and Execute in cmd/root.go:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var version = "0.0.1"
var rootCmd = &cobra.Command{
Use: "textcli",
Version: version,
Short: "String utilities CLI built with Cobra",
Long: "A small Go CLI that reverses, uppercases, and modifies strings using Cobra subcommands.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Run a subcommand: reverse, uppercase, or modify")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}Use is the binary name in help. Version enables Cobra's built-in -v / --version on the root command.
Entry point in main.go:
package main
import "example.com/textcli/cmd"
func main() {
cmd.Execute()
}Before subcommands are registered, root help shows only the built-ins:
go run . --helpA small Go CLI that reverses, uppercases, and modifies strings using Cobra subcommands.
Usage:
textcli [flags]
textcli [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
-h, --help help for textcli
-v, --version version for textcli
Use "textcli [command] --help" for more information about a command.Add subcommands and positional arguments
Add "example.com/textcli/helper" to the import block in cmd/root.go, then create helper/helper.go with the string helpers the commands call:
package helper
import "strings"
func Reverse(s string) string {
rns := []rune(s)
for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 {
rns[i], rns[j] = rns[j], rns[i]
}
return string(rns)
}
func Uppercase(s string) string {
return strings.ToUpper(s)
}
func Modify(s string, opt bool) string {
if opt {
return s + "_" + "MODIFIED"
}
return s + "_" + "modified"
}Cobra derives usage lines from each command's Use field—include the positional placeholder there. Declare option at package scope first (modifyCmd reads it; you bind the flag in the next section):
var option bool
var reverseCmd = &cobra.Command{
Use: "reverse [string]",
Short: "Reverse a string",
Aliases: []string{"rev"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(helper.Reverse(args[0]))
},
}
var uppercaseCmd = &cobra.Command{
Use: "uppercase [string]",
Short: "Uppercase a string",
Aliases: []string{"upper"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(helper.Uppercase(args[0]))
},
}
var modifyCmd = &cobra.Command{
Use: "modify [string]",
Short: "Modify a string",
Aliases: []string{"mod"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(helper.Modify(args[0], option))
},
}Args: cobra.ExactArgs(1) rejects wrong arity before Run runs. Aliases lets users type rev instead of reverse.
Register the subcommands once in init():
func init() {
rootCmd.AddCommand(reverseCmd)
rootCmd.AddCommand(uppercaseCmd)
rootCmd.AddCommand(modifyCmd)
}List commands again:
go run . --helpAvailable Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
modify Modify a string
reverse Reverse a string
uppercase Uppercase a stringInvoke each subcommand with arguments:
go run . reverse helloollehThe rev alias behaves the same:
go run . rev CobraarboCUppercase the argument:
go run . upper golangGOLANGLocal and persistent flags
Cobra has two flag scopes:
command.Flags()— local to that commandcommand.PersistentFlags()— inherited by child commands (shown under Global Flags in subcommand help)
Add a package-level verbose switch:
var verbose boolUpdate only reverseCmd so -V prints a diagnostic before the result (other subcommands still inherit the flag in help, which is enough to show persistence):
var reverseCmd = &cobra.Command{
Use: "reverse [string]",
Short: "Reverse a string",
Aliases: []string{"rev"},
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if verbose {
fmt.Fprintln(os.Stderr, "verbose: reversing", args[0])
}
fmt.Println(helper.Reverse(args[0]))
},
}Open the existing init() and add these two lines at the top—keep your AddCommand calls as they are:
modifyCmd.Flags().BoolVarP(&option, "option", "o", false, "Append MODIFIED suffix when -o is set")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "V", false, "Print extra detail to stderr")BoolVarP stores the parsed value before Run executes. PersistentFlags on rootCmd makes -V / --verbose appear under Global Flags on modify --help even though only reverse reads the variable here.
Default modify output (local -o flag omitted):
go run . modify testtest_modifiedWith the local boolean flag:
go run . modify test -otest_MODIFIEDPersistent -V on reverse writes the diagnostic to stderr and the result to stdout:
go run . reverse hello -Vstderr:
verbose: reversing hello
stdout:
ollehmodify help reflects Use, aliases, the local flag, and the inherited verbose flag:
go run . modify --helpModify a string
Usage:
textcli modify [string] [flags]
Aliases:
modify, mod
Flags:
-h, --help help for modify
-o, --option Append MODIFIED suffix when -o is set
Global Flags:
-V, --verbose Print extra detail to stderrBuilt-in version flag and release builds
Because Version is set on rootCmd, Cobra exposes -v / --version:
go run . --versiontextcli version 0.0.1For release binaries, inject a different version at link time:
go build -o textcli -ldflags="-X 'example.com/textcli/cmd.version=0.0.2'" .Confirm the injected value on the compiled binary:
./textcli --versiontextcli version 0.0.2The -X path must match the package where version is declared. See the cmd/link -X documentation for the exact importpath.name=value syntax.
Scaffold with cobra-cli init
When you prefer generated boilerplate, run cobra-cli init inside a Go module:
mkdir demoapp && cd demoapp
go mod init example.com/demoapp
cobra-cli initTypical scaffold:
demoapp/
cmd/root.go
main.go
LICENSEcobra-cli add fetch creates additional command files under cmd/. Generated projects follow the same main.go → cmd.Execute() pattern; customize Run functions and flags from there.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
unknown command for a subcommand |
Command not added in init() |
Call rootCmd.AddCommand(...) once per subcommand |
| Subcommand listed twice in help | AddCommand called more than once |
Register each command in a single init() |
accepts 1 arg(s), received 0 |
cobra.ExactArgs(1) with no positional |
Pass the string after the subcommand name |
| Flag works on parent but not on child | Created with parent.Flags() |
Use parent.PersistentFlags() when descendants should inherit it |
--version missing |
Version field empty on root |
Set Version: "0.0.1" on rootCmd |
go run cannot find module |
Wrong directory or missing go.mod |
Run from module root; run go mod tidy after adding imports |
Summary
This golang cobra tutorial walked through a complete go cobra example: install github.com/spf13/cobra, define a root command, register cobra subcommands with Use placeholders and ExactArgs, and bind both a local flag on modify and a persistent flag on the root that children inherit. You verified behavior by invoking the CLI with go run, arguments, and flags.
Cobra fits multi-command tools that outgrow the Golang flag package. Keep commands in cmd/, business logic in plain packages, and optionally scaffold with cobra-cli init. Pairing Cobra with Viper is the usual next step for config files and environment variables.
References
- spf13/cobra on GitHub
- Cobra user guide
- spf13/cobra-cli generator
- cmd/link — linker flags including
-X

