Go é a linguagem de fato para ferramentas de linha de comando. Docker, Kubernetes, Terraform, GitHub CLI, Hugo e centenas de outras ferramentas amplamente usadas foram escritas em Go. As razões são concretas: compilação para binário estático sem dependências, distribuição trivial, startup instantâneo, cross-compilation nativa e performance próxima ao C.
Este artigo cobre duas abordagens: o pacote flag da biblioteca padrão para ferramentas simples, e o Cobra para CLIs com subcomandos, flags complexas e help automático — o padrão da indústria para ferramentas profissionais.
O pacote flag: para CLIs simples
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Definindo flags
host := flag.String("host", "localhost", "endereço do servidor")
porta := flag.Int("port", 8080, "porta do servidor")
verbose := flag.Bool("verbose", false, "ativar saída detalhada")
timeout := flag.Duration("timeout", 30*time.Second, "timeout das requisições")
// Personalizar o texto de uso
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Uso: %s [opções]\n\n", os.Args[0])
fmt.Fprintln(os.Stderr, "Opções:")
flag.PrintDefaults()
}
flag.Parse()
// Argumentos posicionais (após as flags)
args := flag.Args()
if *verbose {
fmt.Printf("Conectando em %s:%d (timeout: %v)\n", *host, *porta, *timeout)
fmt.Printf("Argumentos extras: %v\n", args)
}
fmt.Printf("Servidor: %s:%d\n", *host, *porta)
}
# Uso
./app -host=api.exemplo.com -port=443 -verbose arquivo.txt
./app --host api.exemplo.com --port 443
./app -help
O flag funciona bem para ferramentas com um único conjunto de opções. Quando a CLI cresce e precisa de subcomandos como git commit, git push, docker run, o Cobra é a escolha certa.
Cobra: o framework profissional para CLIs
go get github.com/spf13/cobra@latest
go get github.com/spf13/viper@latest # para configuração
A estrutura de um projeto CLI com Cobra:
minha-cli/
├── go.mod
├── main.go
├── cmd/
│ ├── root.go ← comando raiz
│ ├── serve.go ← subcomando: minha-cli serve
│ ├── migrate.go ← subcomando: minha-cli migrate
│ └── usuario/
│ ├── usuario.go ← grupo: minha-cli usuario
│ ├── criar.go ← minha-cli usuario criar
│ └── listar.go ← minha-cli usuario listar
└── internal/
└── config/
└── config.go
Comando raiz
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
verbose bool
formato string
)
var rootCmd = &cobra.Command{
Use: "minha-cli",
Short: "Ferramenta de gerenciamento da aplicação",
Long: `minha-cli é uma ferramenta completa para gerenciar
a aplicação, incluindo servidor, banco de dados e usuários.
Documentação completa em: https://docs.exemplo.com`,
// PersistentPreRunE executa antes de QUALQUER subcomando
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return inicializarConfig()
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(inicializarViper)
// Flags persistentes — disponíveis em todos os subcomandos
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
"arquivo de configuração (padrão: $HOME/.minha-cli.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v",
false, "saída detalhada")
rootCmd.PersistentFlags().StringVar(&formato, "format", "text",
"formato de saída: text, json, yaml")
}
func inicializarViper() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName(".minha-cli")
}
viper.AutomaticEnv() // variáveis de ambiente sobrescrevem config
if err := viper.ReadInConfig(); err == nil && verbose {
fmt.Fprintln(os.Stderr, "Configuração:", viper.ConfigFileUsed())
}
}
func inicializarConfig() error {
if verbose {
fmt.Printf("Usando banco: %s\n", viper.GetString("database.url"))
}
return nil
}
Subcomando: serve
// cmd/serve.go
package cmd
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
serveHost string
servePort int
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Inicia o servidor HTTP",
Long: `Inicia o servidor HTTP da aplicação na porta especificada.`,
Example: ` minha-cli serve
minha-cli serve --port 9090
minha-cli serve --host 0.0.0.0 --port 443`,
RunE: func(cmd *cobra.Command, args []string) error {
return executarServidor(serveHost, servePort)
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().StringVar(&serveHost, "host", "0.0.0.0",
"endereço de escuta")
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080,
"porta de escuta")
// Vincula flags com viper — permite sobrescrever via config ou env
viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port"))
viper.BindPFlag("server.host", serveCmd.Flags().Lookup("host"))
}
func executarServidor(host string, porta int) error {
addr := fmt.Sprintf("%s:%d", host, porta)
mux := http.NewServeMux()
mux.HandleFunc("/saude", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
servidor := &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
fmt.Printf("Servidor iniciado em http://%s\n", addr)
if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "Erro: %v\n", err)
os.Exit(1)
}
}()
<-quit
fmt.Println("\nEncerrando servidor...")
ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelar()
return servidor.Shutdown(ctx)
}
Grupo de subcomandos: usuario
// cmd/usuario/usuario.go
package usuario
import (
"github.com/spf13/cobra"
"minha-cli/cmd"
)
var UsuarioCmd = &cobra.Command{
Use: "usuario",
Short: "Gerencia usuários da aplicação",
Long: `Comandos para criar, listar, atualizar e remover usuários.`,
}
func init() {
cmd.RootCmd.AddCommand(UsuarioCmd) // expondo RootCmd como variável exportada
UsuarioCmd.AddCommand(criarCmd)
UsuarioCmd.AddCommand(listarCmd)
UsuarioCmd.AddCommand(deletarCmd)
}
// cmd/usuario/criar.go
package usuario
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var (
criarNome string
criarEmail string
criarPapel string
)
var criarCmd = &cobra.Command{
Use: "criar",
Short: "Cria um novo usuário",
Example: ` minha-cli usuario criar --nome "João Silva" --email joao@exemplo.com
minha-cli usuario criar --nome "Admin" --email admin@exemplo.com --papel admin`,
RunE: func(cmd *cobra.Command, args []string) error {
return executarCriarUsuario(criarNome, criarEmail, criarPapel)
},
}
func init() {
criarCmd.Flags().StringVarP(&criarNome, "nome", "n", "", "nome completo (obrigatório)")
criarCmd.Flags().StringVarP(&criarEmail, "email", "e", "", "email (obrigatório)")
criarCmd.Flags().StringVar(&criarPapel, "papel", "usuario",
"papel do usuário: usuario, admin, moderador")
// Marca flags como obrigatórias
criarCmd.MarkFlagRequired("nome")
criarCmd.MarkFlagRequired("email")
}
func executarCriarUsuario(nome, email, papel string) error {
papeis := map[string]bool{"usuario": true, "admin": true, "moderador": true}
if !papeis[strings.ToLower(papel)] {
return fmt.Errorf("papel inválido: %s (aceitos: usuario, admin, moderador)", papel)
}
fmt.Printf("Criando usuário...\n")
fmt.Printf(" Nome: %s\n", nome)
fmt.Printf(" Email: %s\n", email)
fmt.Printf(" Papel: %s\n", papel)
// Aqui viria a lógica real de criação
fmt.Println("✓ Usuário criado com sucesso")
return nil
}
// cmd/usuario/listar.go
package usuario
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
listarLimite int
listarPapel string
)
var listarCmd = &cobra.Command{
Use: "listar",
Short: "Lista usuários cadastrados",
RunE: func(cmd *cobra.Command, args []string) error {
return executarListarUsuarios(listarLimite, listarPapel)
},
}
func init() {
listarCmd.Flags().IntVar(&listarLimite, "limite", 20, "máximo de registros")
listarCmd.Flags().StringVar(&listarPapel, "papel", "", "filtrar por papel")
}
type UsuarioListado struct {
ID int `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
Papel string `json:"papel"`
Ativo bool `json:"ativo"`
}
func executarListarUsuarios(limite int, papel string) error {
// Simulação — em produção buscaria do banco
usuarios := []UsuarioListado{
{1, "Ricardo Matos", "ricardo@exemplo.com", "admin", true},
{2, "Ana Silva", "ana@exemplo.com", "usuario", true},
{3, "Carlos Lima", "carlos@exemplo.com", "moderador", false},
}
if papel != "" {
filtrados := usuarios[:0]
for _, u := range usuarios {
if u.Papel == papel {
filtrados = append(filtrados, u)
}
}
usuarios = filtrados
}
if len(usuarios) > limite {
usuarios = usuarios[:limite]
}
// Formato de saída configurável
formato := viper.GetString("format")
switch formato {
case "json":
return json.NewEncoder(os.Stdout).Encode(usuarios)
default:
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNOME\tEMAIL\tPAPEL\tATIVO")
fmt.Fprintln(w, "--\t----\t-----\t-----\t-----")
for _, u := range usuarios {
ativo := "sim"
if !u.Ativo {
ativo = "não"
}
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
u.ID, u.Nome, u.Email, u.Papel, ativo)
}
return w.Flush()
}
}
Subcomando: migrate
// cmd/migrate.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Gerencia migrações do banco de dados",
}
var migrateUpCmd = &cobra.Command{
Use: "up",
Short: "Executa todas as migrações pendentes",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Executando migrações...")
// golang-migrate aqui
fmt.Println("✓ Migrações concluídas")
return nil
},
}
var migrateDownCmd = &cobra.Command{
Use: "down [n]",
Short: "Reverte a última migração (ou N migrações)",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
n := 1
if len(args) > 0 {
fmt.Sscan(args[0], &n)
}
fmt.Printf("Revertendo %d migração(ões)...\n", n)
fmt.Println("✓ Concluído")
return nil
},
}
var migrateStatusCmd = &cobra.Command{
Use: "status",
Short: "Exibe o status das migrações",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Versão atual: 5")
fmt.Println("Pendentes: 0")
return nil
},
}
func init() {
rootCmd.AddCommand(migrateCmd)
migrateCmd.AddCommand(migrateUpCmd)
migrateCmd.AddCommand(migrateDownCmd)
migrateCmd.AddCommand(migrateStatusCmd)
}
main.go: ponto de entrada enxuto
// main.go
package main
import "minha-cli/cmd"
func main() {
cmd.Execute()
}
Completions: autocompletar no shell
O Cobra gera completions para bash, zsh, fish e PowerShell automaticamente:
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Gera script de autocompletar para o shell",
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)
default:
return fmt.Errorf("shell não suportado: %s", args[0])
}
},
}
# Instalando o autocompletar no bash
minha-cli completion bash > /etc/bash_completion.d/minha-cli
# Ou temporariamente na sessão atual
source <(minha-cli completion bash)
Saída colorida e progress
import "github.com/fatih/color"
var (
verde = color.New(color.FgGreen).SprintFunc()
vermelho = color.New(color.FgRed).SprintFunc()
amarelo = color.New(color.FgYellow).SprintFunc()
azul = color.New(color.FgCyan).SprintFunc()
)
func imprimirStatus(ok bool, mensagem string) {
if ok {
fmt.Printf("%s %s\n", verde("✓"), mensagem)
} else {
fmt.Printf("%s %s\n", vermelho("✗"), mensagem)
}
}
func imprimirAviso(mensagem string) {
fmt.Printf("%s %s\n", amarelo("⚠"), mensagem)
}
func imprimirInfo(mensagem string) {
fmt.Printf("%s %s\n", azul("ℹ"), mensagem)
}
Compilando e distribuindo
# Build padrão
go build -o minha-cli .
# Build com versão embutida
go build -ldflags="-X main.versao=1.2.0 -X main.build=$(date -u +%Y%m%d)" -o minha-cli .
# Cross-compilation para múltiplos sistemas
GOOS=linux GOARCH=amd64 go build -o dist/minha-cli-linux-amd64 .
GOOS=darwin GOARCH=arm64 go build -o dist/minha-cli-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o dist/minha-cli-windows-amd64.exe .
// Versão embutida via ldflags
var (
versao = "desenvolvimento"
build = "desconhecido"
)
var versaoCmd = &cobra.Command{
Use: "version",
Short: "Exibe a versão da ferramenta",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("minha-cli v%s (build: %s)\n", versao, build)
},
}
Resumo do que foi coberto
Este artigo apresentou CLIs em Go de forma completa: o pacote flag para ferramentas simples, o Cobra para CLIs com subcomandos e estrutura profissional, comando raiz com flags persistentes, grupos de subcomandos para organização hierárquica, flags obrigatórias, formato de saída configurável, completions para shells, validação de argumentos, saída colorida e compilação cruzada com versão embutida. O próximo artigo explora logging estruturado com slog e Zerolog.
Referências e leituras complementares
-
Documentação do Cobra — Guia oficial com todos os recursos. https://github.com/spf13/cobra
-
Cobra Generator — Ferramenta para gerar estrutura de projeto Cobra. https://github.com/spf13/cobra-cli
-
Viper — Biblioteca de configuração que integra com Cobra. https://github.com/spf13/viper
-
fatih/color — Saída colorida para terminais. https://github.com/fatih/color
-
olekukonko/tablewriter — Tabelas formatadas para saída de CLIs. https://github.com/olekukonko/tablewriter
-
Documentação flag — biblioteca padrão — Referência do pacote flag. https://pkg.go.dev/flag