Skip to content

Use Case: Building a CLI Tool

Go’s single-binary output and cross-compilation make it a natural fit for CLI tools — this is exactly how kubectl, terraform, hugo, and gh are built.

For a single-command tool, flag alone is often enough:

package main
import (
"flag"
"fmt"
)
func main() {
name := flag.String("name", "World", "who to greet")
count := flag.Int("count", 1, "how many times")
flag.Parse()
for i := 0; i < *count; i++ {
fmt.Printf("Hello, %s!\n", *name)
}
}
Terminal window
go run main.go -name=Gopher -count=3

Once you need mytool add, mytool list, mytool remove with their own flags, reach for spf13/cobra — the library behind kubectl and hugo:

var rootCmd = &cobra.Command{Use: "tasks"}
var addCmd = &cobra.Command{
Use: "add [name]",
Short: "Add a new task",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("added:", args[0])
return nil
},
}
func main() {
rootCmd.AddCommand(addCmd)
rootCmd.Execute()
}

This gives you --help text, shell completion generation, and nested subcommands for free.

Well-behaved CLI tools support piping and use exit codes correctly:

scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
process(scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}

Cross-compile for every common target from one machine, no CI matrix needed:

Terminal window
for os in linux darwin windows; do
for arch in amd64 arm64; do
GOOS=$os GOARCH=$arch go build -o "dist/mytool-$os-$arch" .
done
done

Tools like GoReleaser automate this plus GitHub release uploads, checksums, and Homebrew formula generation.