CLI Tools Basics
10 examples to get you started with Go command-line tools - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir clilab && cd clilab && go mod init example.com/clilab. - Save each snippet as
main.go(or separate files in one package) and run withgo run .. - Pass flags after
--when usinggo run:go run . -- -name=Ada.
Basic Examples
1. Hello World with flag
The stdlib flag package parses -name before main logic runs.
package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "world", "greeting target")
flag.Parse()
fmt.Printf("hello, %s\n", *name)
}flag.Stringregisters a pointer updated duringflag.Parse().- Defaults apply when the flag is omitted.
- Unknown flags print usage and exit non-zero unless you customize behavior.
Related: flag Package & POSIX-Style Flags - FlagSet and usage text
2. Boolean and Numeric Flags
Mix Bool, Int, and Duration flags on one command.
package main
import (
"flag"
"fmt"
"time"
)
func main() {
verbose := flag.Bool("v", false, "verbose output")
count := flag.Int("n", 1, "repeat count")
delay := flag.Duration("delay", 0, "pause between lines")
flag.Parse()
for i := 0; i < *count; i++ {
if *verbose {
fmt.Println("tick", i)
}
time.Sleep(*delay)
}
}-vand-v=trueboth set a boolean flag.flag.Durationaccepts300ms,2s,1m.- Parse all flags before reading
flag.Args()for positional arguments.
Related: CLI Design in Go: Single Binary, Fast Startup - why Go fits CLIs
3. Positional Arguments After Flags
flag.Args() returns tokens that were not consumed as flags.
package main
import (
"flag"
"fmt"
)
func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Println("usage: tool <file>...")
return
}
for _, path := range args {
fmt.Println("process", path)
}
}- Flags may appear before or after positionals when using the default set (POSIX-ish).
- Validate arity explicitly; do not assume users pass the right count.
- Print a short usage line when required args are missing.
Related: Color Output & User-Friendly Errors - stderr messaging
4. Custom Usage Function
Replace the default usage text with operator-friendly help.
package main
import (
"flag"
"fmt"
"os"
)
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: %s [flags] <target>\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
}flag.Usageruns on parse errors and when you call it manually.- Write help to stderr so stdout stays pipeable.
PrintDefaultslists registered flags with their defaults.
Related: flag Package & POSIX-Style Flags - usage conventions
5. Dedicated FlagSet per Subcommand
Isolate flags when dispatching on os.Args[1].
package main
import (
"flag"
"fmt"
"os"
}
func main() {
if len(os.Args) < 2 {
fmt.Println("usage: tool <init|run>")
os.Exit(1)
}
switch os.Args[1] {
case "init":
fs := flag.NewFlagSet("init", flag.ExitOnError)
dir := fs.String("dir", ".", "project directory")
_ = fs.Parse(os.Args[2:])
fmt.Println("init in", *dir)
case "run":
fmt.Println("run")
default:
fmt.Println("unknown subcommand")
os.Exit(1)
}
}flag.NewFlagSetavoids collisions between subcommand flags.- Pass
os.Args[2:]so the subcommand name is not re-parsed. flag.ExitOnErrorexits on unknown flags for that subcommand only.
Related: cobra & urfave/cli Command Trees - structured command trees
6. Exit Codes for Scripts
Non-zero exits signal failure to shell scripts and CI.
package main
import (
"errors"
"fmt"
"os"
)
func run() error {
return errors.New("config missing")
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}- Reserve
os.Exitformain; defer functions do not run afteros.Exit. - Document stable exit codes when operators automate remediation.
- Print errors on stderr, not stdout.
Related: Color Output & User-Friendly Errors - exit code conventions
7. Version Flag via ldflags
Inject build metadata at compile time.
package main
import (
"flag"
"fmt"
)
var version = "dev"
func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println(version)
return
}
}- Build with:
go build -ldflags "-X main.version=1.0.0". - The
versionvariable must be a package-levelstring. - Pair
--versionwith CI stamping from git tags.
Related: CLI Tools Best Practices - versioning and distribution
Intermediate Examples
8. Environment Variable Defaults
Read env before flag defaults for twelve-factor style config.
package main
import (
"flag"
"fmt"
"os"
)
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func main() {
def := envOr("API_URL", "http://localhost:8080")
api := flag.String("api", def, "API base URL")
flag.Parse()
fmt.Println(*api)
}- Flags should override env when both are set (check after
Parse). - Document precedence in README and
--help. - Viper automates merging; this shows the manual pattern.
Related: Configuration: Viper, Env & Config Files - layered config
9. Test Flags with Isolated FlagSet
Swap the global flag set in tests to avoid pollution.
package main
import (
"flag"
"testing"
)
func TestGreetFlag(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
name := fs.String("name", "world", "")
err := fs.Parse([]string{"-name", "Ada"})
if err != nil || *name != "Ada" {
t.Fatalf("parse err=%v name=%q", err, *name)
}
}flag.ContinueOnErrorlets tests assert on parse failures.- Prefer testing package
Runfunctions that accept[]stringinstead of globals. - Cobra exposes
SetArgsfor the same isolation pattern.
Related: CLI Tools Best Practices - testing CLIs
10. Minimal cobra Root Command
cobra adds help generation and subcommand routing.
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func main() {
root := &cobra.Command{
Use: "demo",
Short: "demo CLI",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("demo ok")
return nil
},
}
if err := root.Execute(); err != nil {
panic(err)
}
}- Run
go get github.com/spf13/cobra@latestbefore building. RunEreturns errors;Executeprints them and sets exit code 1.- Add child commands with
root.AddCommandfor real tools.
Related: cobra & urfave/cli Command Trees - persistent flags and completion
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).