Building a CLI Tool in Go

March 20, 2026 · 2 min read · Notes on building a small command-line tool — from parsing flags to distributing a binary.

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 libraryflag, os, io get 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.