Ferramentas de linha de comando (CLI) são essenciais no arsenal de qualquer desenvolvedor. Desde gerenciadores de pacotes como npm e pip até infraestrutura como kubectl e terraform, CLIs bem projetadas aumentam a produtividade exponencialmente. Go é a linguagem perfeita para criar CLIs rápidas, portáteis e eficientes.
Neste guia completo, você aprenderá a construir CLI tools profissionais usando Go, desde flags simples até ferramentas complexas com subcomandos e auto-complete.
Por Que Go para CLI Tools?
Vantagens Competitivas
┌─────────────────────────────────────────────────────────────────┐
│ CARACTERÍSTICA │ GO │ PYTHON │ NODE.JS │ RUST │
├─────────────────────────────────────────────────────────────────┤
│ Startup │ ~5ms │ ~100ms │ ~200ms │ ~10ms │
├─────────────────────────────────────────────────────────────────┤
│ Binário único │ ✅ │ ❌ │ ❌ │ ✅ │
├─────────────────────────────────────────────────────────────────┤
│ Cross-compile │ ✅ │ ❌ │ ❌ │ ✅ │
├─────────────────────────────────────────────────────────────────┤
│ Memória │ Baixa │ Alta │ Média │ Baixa │
├─────────────────────────────────────────────────────────────────┤
│ Curva de aprend. │ Média │ Baixa │ Baixa │ Alta │
└─────────────────────────────────────────────────────────────────┘
Casos de Sucesso
- Docker: Container runtime escrito em Go
- Kubernetes: Orquestração de containers
- Hugo: Gerador de sites estáticos
- Terraform: Infrastructure as Code
- Cobra: Framework CLI usado por Kubernetes, etcd, e muitos outros
Fundamentos de CLI em Go
Flags Nativas com flag Package
Go inclui um pacote flag na biblioteca padrão para parsing de argumentos:
// cmd/simple-cli/main.go
package main
import (
"flag"
"fmt"
"os"
)
type Config struct {
Name string
Verbose bool
Count int
Output string
}
func main() {
config := parseFlags()
if config.Verbose {
fmt.Println("Modo verbose ativado")
}
fmt.Printf("Olá, %s! Contagem: %d\n", config.Name, config.Count)
if config.Output != "" {
fmt.Printf("Saída será salva em: %s\n", config.Output)
}
}
func parseFlags() Config {
var config Config
// Definir flags
flag.StringVar(&config.Name, "name", "Mundo", "Nome para saudar")
flag.BoolVar(&config.Verbose, "verbose", false, "Modo detalhado")
flag.IntVar(&config.Count, "count", 1, "Número de saudações")
flag.StringVar(&config.Output, "output", "", "Arquivo de saída (opcional)")
// Parse
flag.Parse()
return config
}
Executando
# Compilar
go build -o mycli cmd/simple-cli/main.go
# Uso básico
./mycli -name="Go" -count=3
# Output: Olá, Go! Contagem: 3
# Modo verbose
./mycli -verbose -name="Desenvolvedor"
# Output:
# Modo verbose ativado
# Olá, Desenvolvedor! Contagem: 1
# Ajuda automática
./mycli -help
# Usage of ./mycli:
# -count int
# Número de saudações (default 1)
# -name string
# Nome para saudar (default "Mundo")
# -output string
# Arquivo de saída (opcional)
# -verbose
# Modo detalhado
Limitações do Pacote flag
O pacote flag é funcional, mas limitado para CLIs complexos:
- ❌ Sem subcomandos nativos
- ❌ Sem auto-complete
- ❌ Sem documentação automática
- ❌ Sintaxe de flags limitada (apenas
-flagou--flag) - ❌ Sem validação de argumentos integrada
Para CLIs profissionais, usamos o Cobra.
Cobra: O Framework CLI Definitivo
Instalação e Setup
# Instalar CLI do Cobra
go install github.com/spf13/cobra-cli@latest
# Inicializar projeto
mkdir myapp && cd myapp
cobra-cli init --pkg-name myapp
# Estrutura criada:
# myapp/
# ├── cmd/
# │ └── root.go
# ├── main.go
# ├── go.mod
# └── LICENSE
Estrutura do Projeto Cobra
myapp/
├── cmd/ # Comandos da aplicação
│ ├── root.go # Comando raiz
│ ├── serve.go # Subcomando: serve
│ ├── config.go # Subcomando: config
│ └── version.go # Subcomando: version
├── internal/ # Código interno
│ ├── config/
│ │ └── config.go
│ └── server/
│ └── server.go
├── pkg/ # Bibliotecas reutilizáveis
│ └── utils/
│ └── utils.go
├── main.go
└── go.mod
Comando Raiz (root.go)
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd representa o comando base
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "Uma aplicação CLI de exemplo",
Long: `MyApp é uma ferramenta de linha de comando que demonstra
as melhores práticas para construir CLIs profissionais em Go.
Complete documentation is available at http://example.com`,
// Executado antes de qualquer subcomando
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Inicialização global
fmt.Println("Inicializando aplicação...")
},
// Executado quando nenhum subcomando é especificado
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Use 'myapp --help' para ver comandos disponíveis")
},
}
// Execute adiciona todos os subcomandos
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Inicialização antes de parsing de flags
cobra.OnInitialize(initConfig)
// Flags persistentes (disponíveis em todos os subcomandos)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
"arquivo de config (default é $HOME/.myapp.yaml)")
// Flags locais (apenas para comando raiz)
rootCmd.Flags().BoolP("toggle", "t", false,
"Help message for toggle")
// Viper binding (para ler de arquivo de config)
viper.BindPFlag("toggle", rootCmd.Flags().Lookup("toggle"))
}
// initConfig lê arquivo de configuração
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".myapp")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
Subcomandos
Criando Subcomandos
# Gerar novo subcomando
cobra-cli add serve
cobra-cli add config
cobra-cli add version
Exemplo: Subcomando serve
// cmd/serve.go
package cmd
import (
"fmt"
"log"
"net/http"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Flags específicas do comando serve
var (
port int
host string
env string
)
// serveCmd representa o comando serve
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Inicia o servidor HTTP",
Long: `Inicia o servidor HTTP com as configurações especificadas.
Exemplos:
# Servidor padrão
myapp serve
# Servidor em porta específica
myapp serve --port=8080
# Modo desenvolvimento
myapp serve --env=development --verbose`,
// Validação de argumentos
Args: cobra.NoArgs, // Não aceita argumentos posicionais
// Execução principal
Run: func(cmd *cobra.Command, args []string) {
startServer()
},
}
func init() {
rootCmd.AddCommand(serveCmd)
// Definir flags
serveCmd.Flags().IntVarP(&port, "port", "p", 8080,
"Porta do servidor")
serveCmd.Flags().StringVar(&host, "host", "localhost",
"Host do servidor")
serveCmd.Flags().StringVarP(&env, "env", "e", "production",
"Ambiente (development|staging|production)")
// Viper binding para ler de config file também
viper.BindPFlag("serve.port", serveCmd.Flags().Lookup("port"))
viper.BindPFlag("serve.host", serveCmd.Flags().Lookup("host"))
viper.BindPFlag("serve.env", serveCmd.Flags().Lookup("env"))
}
func startServer() {
addr := fmt.Sprintf("%s:%d", host, port)
fmt.Printf("🚀 Iniciando servidor em %s (%s)\n", addr, env)
if env == "development" {
fmt.Println("📋 Modo desenvolvimento ativado")
fmt.Println(" - Hot reload habilitado")
fmt.Println(" - Logs detalhados")
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "MyApp Server - %s\n", env)
})
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"env": env,
})
})
log.Printf("Servidor rodando em http://%s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
Exemplo: Subcomando config
// cmd/config.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Gerencia configurações da aplicação",
}
var configGetCmd = &cobra.Command{
Use: "get [chave]",
Short: "Obtém valor de uma configuração",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
value := viper.Get(key)
if value == nil {
fmt.Printf("Configuração '%s' não encontrada\n", key)
return
}
fmt.Printf("%s: %v\n", key, value)
},
}
var configSetCmd = &cobra.Command{
Use: "set [chave] [valor]",
Short: "Define valor de uma configuração",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
key, value := args[0], args[1]
viper.Set(key, value)
if err := viper.WriteConfig(); err != nil {
// Criar config se não existir
viper.SafeWriteConfig()
}
fmt.Printf("✅ %s = %s\n", key, value)
},
}
var configListCmd = &cobra.Command{
Use: "list",
Short: "Lista todas as configurações",
Run: func(cmd *cobra.Command, args []string) {
settings := viper.AllSettings()
if len(settings) == 0 {
fmt.Println("Nenhuma configuração definida")
return
}
fmt.Println("Configurações atuais:")
for k, v := range settings {
fmt.Printf(" %s: %v\n", k, v)
}
},
}
func init() {
rootCmd.AddCommand(configCmd)
// Adicionar sub-subcomandos
configCmd.AddCommand(configGetCmd)
configCmd.AddCommand(configSetCmd)
configCmd.AddCommand(configListCmd)
}
Validação de Argumentos
Tipos de Validação
// Sem argumentos
cobra.NoArgs
// N argumentos exatos
cobra.ExactArgs(2)
// Range de argumentos
cobra.RangeArgs(1, 3)
// Mínimo de argumentos
cobra.MinimumNArgs(1)
// Máximo de argumentos
cobra.MaximumNArgs(3)
// Argumentos arbitrários (default)
cobra.ArbitraryArgs
// Validação customizada
func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("requer exatamente 2 argumentos, recebeu %d", len(args))
}
return nil
}
Validação Customizada
// cmd/user.go
package cmd
import (
"fmt"
"regexp"
"github.com/spf13/cobra"
)
var userCmd = &cobra.Command{
Use: "user [email]",
Short: "Busca usuário por email",
Args: validateEmail,
Run: func(cmd *cobra.Command, args []string) {
email := args[0]
fmt.Printf("Buscando usuário: %s\n", email)
// ...
},
}
func validateEmail(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("uso: myapp user [email]")
}
email := args[0]
emailRegex := regexp.MustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)
if !emailRegex.MatchString(email) {
return fmt.Errorf("'%s' não é um email válido", email)
}
return nil
}
Arquivos de Configuração com Viper
Formatos Suportados
# $HOME/.myapp.yaml
app:
name: "MyApp"
version: "1.0.0"
server:
host: "0.0.0.0"
port: 8080
timeout: 30
database:
host: "localhost"
port: 5432
name: "myapp"
ssl_mode: "disable"
features:
- auth
- logging
- metrics
// .myapp.json
{
"server": {
"port": 8080,
"host": "localhost"
},
"debug": true
}
Uso no Código
package config
import (
"github.com/spf13/viper"
)
type Config struct {
App AppConfig
Server ServerConfig
Database DatabaseConfig
}
type AppConfig struct {
Name string
Version string
}
type ServerConfig struct {
Host string
Port int
Timeout int
}
type DatabaseConfig struct {
Host string
Port int
Name string
SSLMode string
}
func Load() (*Config, error) {
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// Uso
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Servidor: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
}
Exemplo Real: Ferramenta de Backup
Vamos criar uma CLI completa para backups:
// cmd/backup/main.go
package main
import "backup/cmd"
func main() {
cmd.Execute()
}
// cmd/root.go
package cmd
import (
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "backup",
Short: "Ferramenta de backup automático",
Long: `Backup CLI é uma ferramenta poderosa para realizar backups
automáticos de arquivos e bancos de dados.
Suporta:
- Backups locais e remotos (S3, GCS, Azure)
- Compressão automática
- Criptografia
- Agendamento via cron
- Notificações (email, Slack, Discord)`,
}
func Execute() {
rootCmd.Execute()
}
func init() {
rootCmd.AddCommand(createCmd)
rootCmd.AddCommand(listCmd)
rootCmd.AddCommand(restoreCmd)
rootCmd.AddCommand(scheduleCmd)
}
// cmd/create.go
package cmd
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/briandowns/spinner"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var (
source string
destination string
compress bool
encrypt bool
verbose bool
)
var createCmd = &cobra.Command{
Use: "create",
Short: "Cria um novo backup",
Long: `Cria um backup dos arquivos especificados.`,
Example: ` # Backup simples
backup create --source=/home/user/docs --dest=/backup
# Backup com compressão e criptografia
backup create --source=/data --dest=/backup --compress --encrypt
# Backup verbose
backup create -s /data -d /backup -v`,
RunE: func(cmd *cobra.Command, args []string) error {
return runBackup()
},
}
func init() {
createCmd.Flags().StringVarP(&source, "source", "s", "",
"Diretório fonte (obrigatório)")
createCmd.Flags().StringVarP(&destination, "dest", "d", "",
"Diretório de destino (obrigatório)")
createCmd.Flags().BoolVarP(&compress, "compress", "c", false,
"Comprimir backup")
createCmd.Flags().BoolVarP(&encrypt, "encrypt", "e", false,
"Criptografar backup")
createCmd.Flags().BoolVarP(&verbose, "verbose", "v", false,
"Modo verbose")
createCmd.MarkFlagRequired("source")
createCmd.MarkFlagRequired("dest")
}
func runBackup() error {
// Validar source
if _, err := os.Stat(source); os.IsNotExist(err) {
return fmt.Errorf("diretório fonte não existe: %s", source)
}
// Criar diretório de destino se não existir
if err := os.MkdirAll(destination, 0755); err != nil {
return fmt.Errorf("falha ao criar diretório de destino: %w", err)
}
// Gerar nome do backup
timestamp := time.Now().Format("20060102_150405")
backupName := fmt.Sprintf("backup_%s", timestamp)
if compress {
backupName += ".tar.gz"
} else {
backupName += ".tar"
}
if encrypt {
backupName += ".enc"
}
backupPath := filepath.Join(destination, backupName)
// Spinner para feedback visual
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Suffix = " Preparando backup..."
s.Start()
// Simular operações (em produção, implementar backup real)
time.Sleep(500 * time.Millisecond)
if verbose {
s.Suffix = " Listando arquivos..."
}
time.Sleep(500 * time.Millisecond)
if compress {
s.Suffix = " Comprimindo arquivos..."
time.Sleep(1 * time.Second)
}
if encrypt {
s.Suffix = " Criptografando backup..."
time.Sleep(1 * time.Second)
}
s.Suffix = " Finalizando..."
time.Sleep(300 * time.Millisecond)
s.Stop()
// Criar arquivo (placeholder)
f, err := os.Create(backupPath)
if err != nil {
return fmt.Errorf("falha ao criar arquivo de backup: %w", err)
}
f.Close()
// Output bonito
green := color.New(color.FgGreen, color.Bold)
green.Printf("✓ Backup concluído com sucesso!\n\n")
fmt.Printf("Detalhes:\n")
fmt.Printf(" Arquivo: %s\n", backupPath)
fmt.Printf(" Origem: %s\n", source)
fmt.Printf(" Tamanho: %s\n", getSize(backupPath))
if compress {
fmt.Printf(" Compressão: ativada\n")
}
if encrypt {
fmt.Printf(" Criptografia: ativada\n")
}
return nil
}
func getSize(path string) string {
info, err := os.Stat(path)
if err != nil {
return "desconhecido"
}
size := info.Size()
if size < 1024 {
return fmt.Sprintf("%d B", size)
} else if size < 1024*1024 {
return fmt.Sprintf("%.2f KB", float64(size)/1024)
} else {
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
}
}
Auto-Complete
Geração de Scripts
// cmd/completion.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Gera script de auto-complete",
Long: `Gera scripts de auto-complete para shells.
Para usar:
Bash:
$ source <(myapp completion bash)
# Ou permanentemente:
$ myapp completion bash > /etc/bash_completion.d/myapp
Zsh:
$ source <(myapp completion zsh)
# Ou:
$ myapp completion zsh > "${fpath[1]}/_myapp"
Fish:
$ myapp completion fish | source
# Ou:
$ myapp completion fish > ~/.config/fish/completions/myapp.fish
PowerShell:
PS> myapp completion powershell | Out-String | Invoke-Expression
# Ou:
PS> myapp completion powershell > myapp.ps1`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}
Documentação Automática
Gerar Markdown
// tools/gendocs/main.go
package main
import (
"log"
"myapp/cmd"
"github.com/spf13/cobra/doc"
)
func main() {
err := doc.GenMarkdownTree(cmd.RootCmd, "./docs")
if err != nil {
log.Fatal(err)
}
}
Testes
Testando Comandos
// cmd/serve_test.go
package cmd
import (
"bytes"
"testing"
"github.com/spf13/cobra"
)
func TestServeCommand(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "sem flags",
args: []string{},
wantErr: false,
},
{
name: "com porta",
args: []string{"--port=9090"},
wantErr: false,
},
{
name: "porta inválida",
args: []string{"--port=invalid"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := &cobra.Command{}
cmd.AddCommand(serveCmd)
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs(append([]string{"serve"}, tt.args...))
err := cmd.Execute()
if (err != nil) != tt.wantErr {
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Distribuição
Cross-Compilation
# Makefile
BINARY_NAME=backup
VERSION=$(shell git describe --tags --always --dirty)
LDFLAGS=-ldflags "-X cmd.Version=$(VERSION) -s -w"
.PHONY: all build clean
all: build
build:
go build $(LDFLAGS) -o bin/$(BINARY_NAME) ./cmd/backup
# Cross-compilation
build-all:
# Linux
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 ./cmd/backup
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-arm64 ./cmd/backup
# macOS
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-amd64 ./cmd/backup
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-arm64 ./cmd/backup
# Windows
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-windows-amd64.exe ./cmd/backup
clean:
rm -rf bin/
Release com GoReleaser
# .goreleaser.yaml
project_name: backup
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
Checklist para CLI Profissional
- Nome consistente - Comando curto e memorável
-
--helpcompleto - Todos os comandos documentados - Validação - Erros claros para input inválido
- Feedback visual - Progress indicators para operações longas
- Exit codes - 0 para sucesso, não-zero para erros
- Config files - Suporte a YAML/JSON/TOML
- Auto-complete - Scripts para bash/zsh/fish
- Version flag -
--versionfuncional - Logs estruturados - Output consistente
- Testes - Cobertura de comandos principais
Próximos Passos
Continue sua jornada:
- Go e PostgreSQL - Persistência de dados
- Go e gRPC - APIs de alta performance
- Go Observability - Logs e métricas
- Go para Microserviços - Arquitetura distribuída
Crie CLIs poderosas com Go. Compartilhe sua ferramenta!