Building a CLI Tool in Go: From Zero to Distribution
Build a professional CLI tool in Go with Cobra. Covers project structure, flags, config files, testing, cross-compilation, and distribution via Homebrew.
Why Go for CLI Tools?
Go produces single static binaries with no runtime dependencies. No Python version conflicts, no Node.js installs, no JVM. Just one file that works on any machine. This makes Go the dominant language for CLI tools — kubectl, docker, terraform, gh, and hundreds more are written in Go.
A well-structured configuration file is the foundation of reproducible infrastructure.
Project Setup
mkdir myctl && cd myctl
go mod init github.com/yourname/myctl
# Install Cobra (the CLI framework used by kubectl, Hugo, etc.)
go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest # For config files
# Install Cobra CLI generator
go install github.com/spf13/cobra-cli@latest
Project structure:
myctl/
├── cmd/
│ ├── root.go # Root command and global flags
│ ├── deploy.go # 'myctl deploy' subcommand
│ ├── status.go # 'myctl status' subcommand
│ └── config.go # 'myctl config' subcommand
├── internal/
│ ├── client/ # API client
│ ├── config/ # Config management
│ └── output/ # Output formatting
├── main.go
├── go.mod
└── go.sum
The Root Command
Get more insights on Tutorials
Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
verbose bool
output string
)
var rootCmd = &cobra.Command{
Use: "myctl",
Short: "Manage your TechSaaS infrastructure",
Long: "myctl is a CLI tool for managing TechSaaS deployments, services, and configurations.",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default $HOME/.myctl.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "text", "output format (text, json, yaml)")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, _ := os.UserHomeDir()
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".myctl")
}
viper.AutomaticEnv()
viper.ReadInConfig()
}
Adding Subcommands
// cmd/deploy.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var deployCmd = &cobra.Command{
Use: "deploy [service]",
Short: "Deploy a service to your infrastructure",
Long: "Deploy a Docker service with automatic Traefik routing and health checks.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
service := args[0]
env, _ := cmd.Flags().GetString("env")
dryRun, _ := cmd.Flags().GetBool("dry-run")
if dryRun {
fmt.Printf("DRY RUN: Would deploy %s to %s\n", service, env)
return nil
}
fmt.Printf("Deploying %s to %s...\n", service, env)
// Your deployment logic here
if err := performDeploy(service, env); err != nil {
return fmt.Errorf("deployment failed: %w", err)
}
fmt.Printf("Successfully deployed %s\n", service)
return nil
},
}
func init() {
rootCmd.AddCommand(deployCmd)
deployCmd.Flags().StringP("env", "e", "production", "target environment")
deployCmd.Flags().Bool("dry-run", false, "show what would be deployed")
}
// cmd/status.go
package cmd
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
"github.com/spf13/cobra"
)
type ServiceStatus struct {
Name string `json:"name"`
Status string `json:"status"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
Uptime string `json:"uptime"`
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "Show status of all services",
RunE: func(cmd *cobra.Command, args []string) error {
services := getServiceStatuses()
format, _ := cmd.Flags().GetString("output")
switch format {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(services)
default:
return printStatusTable(services)
}
},
}
func printStatusTable(services []ServiceStatus) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tSTATUS\tCPU\tMEMORY\tUPTIME")
for _, s := range services {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
s.Name, s.Status, s.CPU, s.Memory, s.Uptime)
}
return w.Flush()
}
func init() {
rootCmd.AddCommand(statusCmd)
}
Docker Compose brings up your entire stack with a single command.
Interactive Input and Progress
// internal/ui/prompt.go
package ui
import (
"bufio"
"fmt"
"os"
"strings"
)
func Confirm(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s [y/N]: ", prompt)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(strings.ToLower(input)) == "y"
}
func Spinner(done chan bool, message string) {
chars := []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}
i := 0
for {
select {
case <-done:
fmt.Printf("\r✓ %s\n", message)
return
default:
fmt.Printf("\r%c %s", chars[i%len(chars)], message)
i++
time.Sleep(80 * time.Millisecond)
}
}
}
Testing CLI Commands
// cmd/deploy_test.go
package cmd
import (
"bytes"
"testing"
)
func TestDeployDryRun(t *testing.T) {
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"deploy", "nginx", "--dry-run"})
err := rootCmd.Execute()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := buf.String()
if !strings.Contains(output, "DRY RUN") {
t.Errorf("expected dry run output, got: %s", output)
}
}
func TestDeployRequiresServiceName(t *testing.T) {
rootCmd.SetArgs([]string{"deploy"})
err := rootCmd.Execute()
if err == nil {
t.Fatal("expected error for missing service name")
}
}
Cross-Compilation and Release
Go makes cross-compilation trivial:
# Build for all platforms
GOOS=linux GOARCH=amd64 go build -o dist/myctl-linux-amd64
GOOS=linux GOARCH=arm64 go build -o dist/myctl-linux-arm64
GOOS=darwin GOARCH=amd64 go build -o dist/myctl-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o dist/myctl-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o dist/myctl-windows-amd64.exe
Or use GoReleaser for automated releases:
# .goreleaser.yml
project_name: myctl
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
archives:
- format: tar.gz
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
format: zip
brews:
- repository:
owner: yourname
name: homebrew-tap
homepage: https://github.com/yourname/myctl
description: Manage your TechSaaS infrastructure
install: |
bin.install "myctl"
# Tag and release
git tag v1.0.0
git push --tags
goreleaser release --clean
Free Resource
Free Cloud Architecture Checklist
A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.
Users can then install via:
# Homebrew (macOS/Linux)
brew install yourname/tap/myctl
# Go install
go install github.com/yourname/myctl@latest
# Direct download
curl -L https://github.com/yourname/myctl/releases/latest/download/myctl-$(uname -s)-$(uname -m) -o myctl
chmod +x myctl
Docker Compose defines your entire application stack in a single YAML file.
Shell Completions
Cobra generates completions for bash, zsh, fish, and PowerShell:
// cmd/completion.go
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completions",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
return rootCmd.GenZshCompletion(os.Stdout)
case "fish":
return rootCmd.GenFishCompletion(os.Stdout, true)
case "powershell":
return rootCmd.GenPowerShellCompletion(os.Stdout)
}
return fmt.Errorf("unsupported shell: %s", args[0])
},
}
Go's combination of static binaries, fast compilation, excellent standard library, and the Cobra framework make it the ideal choice for CLI tools. At TechSaaS, our internal tooling is all built in Go for exactly these reasons.
Related Service
Cloud Solutions
Let our experts help you build the right technology strategy for your business.
Need help with tutorials?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.
We Will Build You a Demo Site — For Free
Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.
No spam. No contracts. Just a free demo.