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.
Flags with the standard library
Section titled “Flags with the standard library”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) }}go run main.go -name=Gopher -count=3Subcommands with a real CLI library
Section titled “Subcommands with a real CLI library”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.
Reading from stdin and exit codes
Section titled “Reading from stdin and exit codes”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)}Shipping releases
Section titled “Shipping releases”Cross-compile for every common target from one machine, no CI matrix needed:
for os in linux darwin windows; do for arch in amd64 arm64; do GOOS=$os GOARCH=$arch go build -o "dist/mytool-$os-$arch" . donedoneTools like GoReleaser automate this plus GitHub release uploads, checksums, and Homebrew formula generation.