Criando Aplicações CLI em Go com Cobra e Viper

Desenvolvimento de CLI em Go com os frameworks Cobra e Viper

Conteúdo da página

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

Go emergiu como uma excelente linguagem para construir ferramentas de CLI devido ao seu desempenho, implantação simples e suporte a várias plataformas.

Tetris

Por que escolher Go para Aplicações de CLI

Go oferece vantagens convincentes para o desenvolvimento de CLI:

  • Distribuição em Binário Único: Sem dependências de tempo de execução ou gerenciadores de pacotes necessários
  • Execução Rápida: Compilação nativa oferece excelente desempenho
  • Suporte Multiplataforma: Compilação fácil para Linux, macOS, Windows e mais
  • Biblioteca Padrão Robusta: Ferramentas ricas para E/S de arquivos, rede e processamento de texto
  • Concorrência: Goroutines nativas para operações paralelas
  • Tipagem Estática: Detecta erros em tempo de compilação

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

Introdução ao Cobra

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

Principais Funcionalidades do Cobra

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

Tratamento de Flags: Flags locais e persistentes com parsing automático e conversão de tipo.

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

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

Completas de Shell: Gera scripts de conclusão para bash, zsh, fish e PowerShell.

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

Introdução ao Viper

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

Principais Funcionalidades 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 remotos (etcd, Consul)
  • Valores padrão

Ordem de Prioridade de 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 de 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 Tipo: Conversão automática para vários tipos Go (string, int, bool, duration, etc.).

Primeiros Passos: 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 CLI Cobra para criar a estrutura:

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

Construindo Sua Primeira Aplicação de CLI

Vamos construir um exemplo prático: uma ferramenta de CLI de gerenciamento de tarefas 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 Raiz com Integração 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 CLI simples de gerenciamento de tarefas",
    Long: `MyTasks é um gerenciador de tarefas de 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 da tarefa
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
    
    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", 
        "Prioridade da tarefa (low, medium, high)")
}

Comando Listar

// 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 das tarefas
    },
}

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

Implementando Persistência de Dados

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

  • SQLite: Banco de dados leve e sem servidor, perfeito para ferramentas de 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 chave-valor

Se você estiver trabalhando com bancos de dados relacionais como PostgreSQL, você quererá usar um ORM ou construtor de consultas. Para uma comparação abrangente de bibliotecas de banco de dados Go, veja nosso guia sobre Comparando ORMs 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

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

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

Recarga 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
    })
}

Melhores Práticas

1. Usar o Gerador Cobra para Consistência

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

2. Organizar Comandos em Arquivos Separados

Mantenha cada comando em seu próprio arquivo sob o diretório cmd/ para facilitar a manutenção.

3. Aproveitar 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. Implementar Tratamento de Erros Adequado

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. Fornecer 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:
  - development (dev)
  - staging
  - production (prod)`,
    Example: `  myapp deploy staging
  myapp deploy production --version=1.2.3`,
}

6. Definir Padrões Sensatos

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

7. Validar 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 de 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 Completas de Shell

Cobra pode gerar scripts de conclusão 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 conclusão",
    Long: `Para carregar as conclusões:

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)
}

Compilação e Distribuição

Compilar 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

Reduzir Tamanho do Binário

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

Adicionar 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("Compilado: %s\n", BuildTime)
    },
}

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

Compilar 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 containers
  • Helm: Gerenciador de pacotes do Kubernetes
  • Skaffold: Ferramenta de fluxo de trabalho de desenvolvimento do Kubernetes
  • Cobra CLI: Auto-hospedagem - Cobra usa a si mesma!

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 de CLI que interajam com grandes modelos de linguagem, consulte nosso guia sobre Constraining LLMs with Structured Output using Ollama and 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 profissionais de CLI em Go. O Cobra gerencia a estrutura de comandos, parsing 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 de CLI que são:

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

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

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.