Building a CLI Tool in Go
I recently needed a small CLI tool to automate something I was doing manually every day. Instead of reaching for a shell script, I wrote it in Go. Here’s what I learned.
Why Go for CLIs?
A few reasons:
- Single binary — no runtime, no dependencies, just copy and run
- Fast startup — unlike Python or Node, there’s no interpreter warm-up
- Good standard library —
flag,os,ioget you a long way
The tradeoff is verbosity, but for a tool I’ll use daily, that’s fine.
Project structure
mytool/
├── main.go
├── cmd/
│ ├── root.go
│ └── run.go
└── go.mod
For small tools I skip cobra and just use flag from stdlib. For anything with subcommands, cobra is worth the dependency.
Parsing flags
package main
import (
"flag"
"fmt"
"os"
)
func main() {
verbose := flag.Bool("verbose", false, "Enable verbose output")
output := flag.String("out", "stdout", "Output destination")
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "error: no input provided")
os.Exit(1)
}
if *verbose {
fmt.Printf("output: %s\n", *output)
fmt.Printf("args: %v\n", args)
}
// ... do the thing
}
Reading from stdin
One pattern I use constantly — accept input from a file argument or stdin:
func getInput(args []string) (io.Reader, error) {
if len(args) > 0 {
return os.Open(args[0])
}
// Check if stdin has data
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
return os.Stdin, nil
}
return nil, fmt.Errorf("no input: provide a file or pipe data via stdin")
}
This makes the tool composable — it plays nicely with pipes and shell scripts.
Distributing the binary
For personal tools, go install is enough:
go install github.com/you/mytool@latest
For something others will use, goreleaser handles cross-compilation and GitHub releases with minimal config.
Takeaways
The standard library gets you 80% of the way. Reach for cobra when you need subcommands. Build tools that pipe — stdin/stdout composition is underrated.