Criar uma CLI com Golang é um dos melhores projetos para aprender Go de forma prática: você treina main, pacotes, flags, erros, testes, build e distribuição sem depender de servidor web, banco de dados ou framework pesado. Uma boa ferramenta de linha de comando também vira peça real de portfólio, porque resolve tarefas repetitivas no terminal e mostra cuidado com UX de desenvolvedor.
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 é uma linguagem forte 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.
Se você ainda está começando na linguagem, siga antes a trilha de Go tutorial em português e revise Go Modules. Se já sabe criar um programa básico, este tutorial mostra o caminho natural: primeiro flag, depois organização de comandos, depois Cobra, configuração, testes e release.
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!