Construindo Aplicativos CLI em Go com Cobra & Viper

Desenvolvimento de CLI em Go com os frameworks Cobra e Viper

Conteúdo da página

Aplicações de interface de linha de comandos (CLI) são ferramentas essenciais para desenvolvedores, administradores de sistemas e profissionais de DevOps. Duas bibliotecas Go tornaram-se o padrão de fato para desenvolvimento de CLI em Go: Cobra para estrutura de comandos e Viper para gerenciamento de configuração.

O Go surgiu como uma excelente linguagem para construir ferramentas CLI devido ao seu desempenho, implantação simples e suporte multiplataforma.

Tetris

Por que escolher o Go para aplicações CLI

O Go oferece vantagens convincentes para o desenvolvimento de CLI:

  • Distribuição de único binário: Nenhuma dependência de tempo de execução ou gerenciadores de pacotes necessários
  • Execução rápida: Compilação nativa fornece excelente desempenho
  • Suporte multiplataforma: Compilação fácil para Linux, macOS, Windows e mais
  • Biblioteca padrão robusta: Ferramentas ricas para E/S de arquivo, rede e processamento de texto
  • Concorrência: Goroutines integradas para operações paralelas
  • Tipagem estática: Captura erros no momento da compilação

Ferramentas CLI populares construídas com Go incluem Docker, Kubernetes (kubectl), Hugo, Terraform e GitHub CLI. Se você é novo no Go ou precisa de uma referência rápida, consulte nossa Folha de Dicas do Go para sintaxe e padrões essenciais do Go.

Introdução ao Cobra

O Cobra é uma biblioteca que fornece uma interface simples para criar aplicações CLI poderosas e modernas. Criado por Steve Francia (spf13), o mesmo autor por trás do Hugo e Viper, o Cobra é usado em muitos dos projetos Go mais populares.

Principais características do Cobra

Estrutura de Comando: O Cobra implementa o padrão de comando, permitindo que você crie aplicações com comandos e subcomandos (como git commit ou docker run).

Manipulação de Flags: Flags locais e persistentes com análise automática e conversão de tipos.

Ajuda Automática: Gera automaticamente texto de ajuda e informações de uso.

Sugestões Inteligentes: Fornece sugestões quando os usuários cometem erros de digitação (“Você quis dizer ‘status’?”).

Completions de Shell: Gera scripts de completions para bash, zsh, fish e PowerShell.

Saída Flexível: Funciona perfeitamente com formatadores personalizados e estilos de saída.

Introdução ao Viper

O Viper é uma solução completa de configuração para aplicações Go, projetada para funcionar de forma integrada com o Cobra. Ele lida com a configuração de múltiplas fontes com uma ordem clara de precedência.

Principais características do Viper

Múltiplas Fontes de Configuração:

  • Arquivos de configuração (JSON, YAML, TOML, HCL, INI, envfile, propriedades Java)
  • Variáveis de ambiente
  • Flags de linha de comando
  • Sistemas de configuração remota (etcd, Consul)
  • Valores padrão

Ordem de Precedência da Configuração:

  1. Chamadas explícitas para Set
  2. Flags de linha de comando
  3. Variáveis de ambiente
  4. Arquivo de configuração
  5. Armazenamento chave/valor
  6. Valores padrão

Monitoramento em Tempo Real: Monitora arquivos de configuração e recarrega automaticamente quando ocorrem alterações.

Conversão de Tipos: Conversão automática para diversos tipos Go (string, int, bool, duração, etc.).

Começando: Instalação

Primeiro, inicialize um novo módulo Go e instale ambas as bibliotecas:

go mod init myapp
go get -u github.com/spf13/cobra@latest
go get -u github.com/spf13/viper@latest

Opcionalmente, instale o gerador de CLI do Cobra para scaffolding:

go install github.com/spf13/cobra-cli@latest

Construindo sua primeira aplicação CLI

Vamos construir um exemplo prático: uma ferramenta de gerenciamento de tarefas CLI com suporte a configuração.

Estrutura do Projeto

mytasks/
├── cmd/
│   ├── root.go
│   ├── add.go
│   ├── list.go
│   └── complete.go
├── config/
│   └── config.go
├── main.go
└── config.yaml

Ponto de entrada principal

// main.go
package main

import "mytasks/cmd"

func main() {
    cmd.Execute()
}

Comando principal com integração do Viper

// cmd/root.go
package cmd

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "mytasks",
    Short: "Um simples gerenciador de tarefas CLI",
    Long: `MyTasks é um gerenciador de tarefas CLI que ajuda você a organizar 
suas tarefas diárias com facilidade. Construído com Cobra e Viper.`,
}

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", "", 
        "arquivo de configuração (padrão é $HOME/.mytasks.yaml)")
    rootCmd.PersistentFlags().String("db", "", 
        "localização do arquivo de banco de dados")
    
    viper.BindPFlag("database", rootCmd.PersistentFlags().Lookup("db"))
}

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        
        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".mytasks")
    }
    
    viper.SetEnvPrefix("MYTASKS")
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Usando arquivo de configuração:", viper.ConfigFileUsed())
    }
}

Adicionando subcomandos

// cmd/add.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var priority string

var addCmd = &cobra.Command{
    Use:   "add [descrição da tarefa]",
    Short: "Adicionar uma nova tarefa",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        task := args[0]
        db := viper.GetString("database")
        
        fmt.Printf("Adicionando tarefa: %s\n", task)
        fmt.Printf("Prioridade: %s\n", priority)
        fmt.Printf("Banco de dados: %s\n", db)
        
        // Aqui você implementaria o armazenamento real de tarefas
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
    
    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", 
        "prioridade da tarefa (baixa, média, alta)")
}

Comando de Lista

// cmd/list.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var showCompleted bool

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "Listar todas as tarefas",
    Run: func(cmd *cobra.Command, args []string) {
        db := viper.GetString("database")
        
        fmt.Printf("Listando tarefas de: %s\n", db)
        fmt.Printf("Mostrar concluídas: %v\n", showCompleted)
        
        // Aqui você implementaria a listagem real de tarefas
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    
    listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false, 
        "Mostrar tarefas concluídas")
}

Implementando a persistência de dados

Para uma ferramenta de gerenciamento de tarefas CLI em produção, você precisará implementar o armazenamento real de dados. Embora estejamos usando uma configuração simples de caminho do banco de dados aqui, você tem várias opções para persistir dados:

  • SQLite: Banco de dados leve e sem servidor perfeito para ferramentas CLI
  • PostgreSQL/MySQL: Bancos de dados completos para aplicações mais complexas
  • Arquivos JSON/YAML: Armazenamento baseado em arquivos simples para necessidades leves
  • Bancos de dados embutidos: BoltDB, BadgerDB para armazenamento de chave-valor

Se você estiver trabalhando com bancos de dados relacionais como PostgreSQL, você quererá usar uma ORM ou construtor de consultas. Para uma comparação abrangente das bibliotecas de banco de dados do Go, veja nosso guia sobre Comparando ORMs do Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc.

Configuração Avançada com Viper

Exemplo de Arquivo de Configuração

# .mytasks.yaml
database: ~/.mytasks.db
format: table
colors: true

notifications:
  enabled: true
  sound: true

priorities:
  high: red
  medium: yellow
  low: green

Lendo Configuração Aninhada

func getNotificationSettings() {
    enabled := viper.GetBool("notifications.enabled")
    sound := viper.GetBool("notifications.sound")
    
    fmt.Printf("Notificações habilitadas: %v\n", enabled)
    fmt.Printf("Som habilitado: %v\n", sound)
}

func getPriorityColors() map[string]string {
    return viper.GetStringMapString("priorities")
}

Variáveis de Ambiente

O Viper lê automaticamente variáveis de ambiente com o prefixo configurado:

export MYTASKS_DATABASE=/tmp/tasks.db
export MYTASKS_NOTIFICATIONS_ENABLED=false
mytasks list

Recarregamento de Configuração em Tempo Real

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Arquivo de configuração alterado:", e.Name)
        // Recarregar componentes dependentes da configuração
    })
}

Boas Práticas

1. Use o Gerador do Cobra para Consistência

cobra-cli init
cobra-cli add serve
cobra-cli add create

2. Organize os Comandos em Arquivos Separados

Mantenha cada comando em seu próprio arquivo sob o diretório cmd/ para manutenibilidade.

3. Aproveite as Flags Persistentes

Use flags persistentes para opções que se aplicam a todos os subcomandos:

rootCmd.PersistentFlags().StringP("output", "o", "text", 
    "Formato de saída (text, json, yaml)")

4. Implemente um Tratamento Adequado de Erros

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "Minha aplicação",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("falha ao fazer algo: %w", err)
        }
        return nil
    },
}

5. Forneça Texto de Ajuda Significativo

var cmd = &cobra.Command{
    Use:   "deploy [ambiente]",
    Short: "Implantar aplicação no ambiente especificado",
    Long: `Implanta builds e implanta sua aplicação no 
ambiente especificado. Ambientes suportados são:
  - desenvolvimento (dev)
  - staging
  - produção (prod)`,
    Example: `  myapp deploy staging
  myapp deploy production --version=1.2.3`,
}

6. Defina Valores Padrão Sensatos

func init() {
    viper.SetDefault("port", 8080)
    viper.SetDefault("timeout", "30s")
    viper.SetDefault("retries", 3)
}

7. Valide a Configuração

func validateConfig() error {
    port := viper.GetInt("port")
    if port < 1024 || port > 65535 {
        return fmt.Errorf("porta inválida: %d", port)
    }
    return nil
}

Testando Aplicações CLI

Testando Comandos

// cmd/root_test.go
package cmd

import (
    "bytes"
    "testing"
)

func TestRootCommand(t *testing.T) {
    cmd := rootCmd
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetArgs([]string{"--help"})
    
    if err := cmd.Execute(); err != nil {
        t.Fatalf("execução do comando falhou: %v", err)
    }
}

Testando com Diferentes Configurações

func TestWithConfig(t *testing.T) {
    viper.Set("database", "/tmp/test.db")
    viper.Set("debug", true)
    
    // Execute seus testes
    
    viper.Reset() // Limpeza
}

Gerando Completions de Shell

O Cobra pode gerar scripts de completions para vários shells:

// cmd/completion.go
package cmd

import (
    "os"
    "github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Gerar script de completions",
    Long: `Para carregar completions:

Bash:
  $ source <(mytasks completion bash)

Zsh:
  $ source <(mytasks completion zsh)

Fish:
  $ mytasks completion fish | source

PowerShell:
  PS> mytasks completion powershell | Out-String | Invoke-Expression
`,
    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)
        }
    },
}

func init() {
    rootCmd.AddCommand(completionCmd)
}

Construção e Distribuição

Construa para Múltiplas Plataformas

# Linux
GOOS=linux GOARCH=amd64 go build -o mytasks-linux-amd64

# macOS
GOOS=darwin GOARCH=amd64 go build -o mytasks-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o mytasks-darwin-arm64

# Windows
GOOS=windows GOARCH=amd64 go build -o mytasks-windows-amd64.exe

Reduza o Tamanho do Binário

go build -ldflags="-s -w" -o mytasks

Adicione Informações de Versão

// cmd/version.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var (
    Version   = "dev"
    Commit    = "none"
    BuildTime = "unknown"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Imprimir informações de versão",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Versão: %s\n", Version)
        fmt.Printf("Commit: %s\n", Commit)
        fmt.Printf("Construído: %s\n", BuildTime)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}

Construa com informações de versão:

go build -ldflags="-X 'mytasks/cmd.Version=1.0.0' \
    -X 'mytasks/cmd.Commit=$(git rev-parse HEAD)' \
    -X 'mytasks/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
    -o mytasks

Exemplos do Mundo Real

Ferramentas de código aberto populares construídas com Cobra e Viper:

  • kubectl: Ferramenta de linha de comando do Kubernetes
  • Hugo: Gerador de sites estáticos
  • GitHub CLI (gh): CLI oficial do GitHub
  • Docker CLI: Gerenciamento de contêineres
  • Helm: Gerenciador de pacotes do Kubernetes
  • Skaffold: Ferramenta de fluxo de trabalho de desenvolvimento do Kubernetes
  • Cobra CLI: Auto-hospedado - o Cobra usa ele mesmo!

Além das ferramentas tradicionais de DevOps, as capacidades de CLI do Go se estendem a aplicações de IA e aprendizado de máquina. Se você estiver interessado em construir ferramentas CLI que interagem com modelos de linguagem grandes, consulte nosso guia sobre Restringindo LLMs com Saída Estruturada usando Ollama e Go, que mostra como construir aplicações Go que trabalham com modelos de IA.

Recursos Úteis

Conclusão

Cobra e Viper juntos fornecem uma base poderosa para construir aplicações CLI profissionais no Go. O Cobra lida com a estrutura de comandos, análise de flags e geração de ajuda, enquanto o Viper gerencia a configuração de múltiplas fontes com precedência inteligente.

Essa combinação permite que você crie ferramentas CLI que sejam:

  • Fáceis de usar com comandos intuitivos e ajuda automática
  • Flexíveis com múltiplas fontes de configuração
  • Profissionais com completions de shell e tratamento adequado de erros
  • Manteníveis com organização de código limpa
  • Portáveis entre diferentes plataformas

Seja você construindo ferramentas para desenvolvedores, utilitários de sistema ou automação de DevOps, o Cobra e o Viper fornecem a base sólida que você precisa para criar aplicações CLI que os usuários amarão.