← Voltar para o blog

Como Criar CLIs Profissionais em Go com Cobra e Viper

Aprenda a criar CLIs profissionais em Go com Cobra e Viper: comandos, flags, configuracao YAML, shell completion e distribuicao com GoReleaser. Guia pratico.

Go e a linguagem dominante no ecossistema de ferramentas de linha de comando. Docker, kubectl, GitHub CLI (gh), Terraform, Hugo – todas escritas em Go. Nao e coincidencia: binarios estaticos sem dependencias, compilacao cruzada trivial e startup instantaneo fazem de Go a escolha natural para CLIs.

As duas bibliotecas que sustentam a maioria dessas ferramentas sao Cobra (comandos e subcomandos) e Viper (configuracao). Neste guia, voce vai construir uma CLI profissional do zero usando ambas.

Por que Go para CLIs

Antes de mergulhar no codigo, entenda por que Go domina o espaco de CLIs:

  • Binario unico – sem runtime, sem dependencias, sem pip install ou npm install
  • Compilacao cruzadaGOOS=windows GOARCH=amd64 go build gera um .exe a partir do Linux
  • Startup instantaneo – CLIs Go iniciam em milissegundos, nao segundos
  • Concorrencia nativagoroutines para operacoes paralelas (downloads, builds, deploys)
  • Standard library poderosaflag, os, io, filepath cobrem 80% dos casos

Para CLIs simples, a standard library basta. Para CLIs com subcomandos, flags complexas e configuracao, Cobra e Viper sao o padrao da industria.

Configurando o Projeto com cobra-cli

Instale o gerador de codigo do Cobra:

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

Crie o projeto e inicialize:

mkdir taskctl && cd taskctl
go mod init github.com/seu-usuario/taskctl
cobra-cli init

Isso gera a estrutura basica:

taskctl/
  cmd/
    root.go
  main.go
  go.mod
  go.sum

O main.go e minimo – apenas chama cmd.Execute(). Toda a logica fica nos arquivos dentro de cmd/.

Criando Comandos e Subcomandos

Adicione comandos com o gerador:

cobra-cli add add      # taskctl add
cobra-cli add list     # taskctl list
cobra-cli add done     # taskctl done

Agora implemente o comando add em cmd/add.go:

package cmd

import (
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/spf13/cobra"
)

type Task struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Priority  string    `json:"priority"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"created_at"`
}

var priority string

var addCmd = &cobra.Command{
	Use:   "add [titulo da tarefa]",
	Short: "Adiciona uma nova tarefa",
	Long: `Adiciona uma nova tarefa a lista com titulo e prioridade opcional.

Exemplos:
  taskctl add "Estudar Go"
  taskctl add "Deploy em producao" --priority alta`,
	Args: cobra.MinimumNArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		tasks, err := loadTasks()
		if err != nil {
			return fmt.Errorf("erro ao carregar tarefas: %w", err)
		}

		task := Task{
			ID:        len(tasks) + 1,
			Title:     args[0],
			Priority:  priority,
			Done:      false,
			CreatedAt: time.Now(),
		}

		tasks = append(tasks, task)

		if err := saveTasks(tasks); err != nil {
			return fmt.Errorf("erro ao salvar: %w", err)
		}

		fmt.Printf("Tarefa #%d criada: %s [%s]\n", task.ID, task.Title, task.Priority)
		return nil
	},
}

func init() {
	rootCmd.AddCommand(addCmd)
	addCmd.Flags().StringVarP(&priority, "priority", "p", "media",
		"Prioridade da tarefa (baixa, media, alta)")
}

func loadTasks() ([]Task, error) {
	data, err := os.ReadFile("tasks.json")
	if os.IsNotExist(err) {
		return []Task{}, nil
	}
	if err != nil {
		return nil, err
	}
	var tasks []Task
	return tasks, json.Unmarshal(data, &tasks)
}

func saveTasks(tasks []Task) error {
	data, err := json.MarshalIndent(tasks, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile("tasks.json", data, 0644)
}

Note o uso de RunE em vez de Run – isso permite retornar erros que o Cobra formata automaticamente. Uma boa pratica alinhada com o tratamento de erros idiomatico de Go.

Comando list com Formatacao

package cmd

import (
	"fmt"
	"os"
	"text/tabwriter"

	"github.com/spf13/cobra"
)

var showAll bool

var listCmd = &cobra.Command{
	Use:   "list",
	Short: "Lista todas as tarefas",
	Aliases: []string{"ls"},
	RunE: func(cmd *cobra.Command, args []string) error {
		tasks, err := loadTasks()
		if err != nil {
			return err
		}

		if len(tasks) == 0 {
			fmt.Println("Nenhuma tarefa encontrada.")
			return nil
		}

		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		fmt.Fprintln(w, "ID\tTITULO\tPRIORIDADE\tSTATUS")
		fmt.Fprintln(w, "--\t------\t----------\t------")

		for _, t := range tasks {
			if !showAll && t.Done {
				continue
			}
			status := "pendente"
			if t.Done {
				status = "concluida"
			}
			fmt.Fprintf(w, "%d\t%s\t%s\t%s\n",
				t.ID, t.Title, t.Priority, status)
		}
		w.Flush()
		return nil
	},
}

func init() {
	rootCmd.AddCommand(listCmd)
	listCmd.Flags().BoolVarP(&showAll, "all", "a", false,
		"Mostra tarefas concluidas tambem")
}

O uso de tabwriter garante alinhamento profissional na saida – mesmo padrao que kubectl get pods usa.

Flags: Persistent, Local e Required

Cobra distingue tres tipos de flags:

// Local flag -- so disponivel neste comando
addCmd.Flags().StringVarP(&priority, "priority", "p", "media", "Prioridade")

// Persistent flag -- disponivel neste comando e todos os subcomandos
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Saida detalhada")

// Required flag -- cobra retorna erro se nao fornecida
addCmd.Flags().StringVar(&assignee, "assignee", "", "Responsavel pela tarefa")
addCmd.MarkFlagRequired("assignee")

Configuracao com Viper

Viper gerencia configuracao de multiplas fontes com precedencia clara:

  1. Flags da linha de comando (maior prioridade)
  2. Variaveis de ambiente
  3. Arquivo de configuracao (YAML, JSON, TOML)
  4. Valores padrao (menor prioridade)

Configure no cmd/root.go:

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var cfgFile string

var rootCmd = &cobra.Command{
	Use:   "taskctl",
	Short: "Gerenciador de tarefas via linha de comando",
	Long: `taskctl e uma CLI para gerenciar tarefas do seu dia a dia.
Suporta criacao, listagem, conclusao e configuracao via YAML.`,
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize(initConfig)
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
		"Arquivo de configuracao (padrao: $HOME/.taskctl.yaml)")
	rootCmd.PersistentFlags().String("format", "table",
		"Formato de saida (table, json, yaml)")

	// Bind flag ao Viper -- agora viper.GetString("format") funciona
	viper.BindPFlag("format", rootCmd.PersistentFlags().Lookup("format"))
}

func initConfig() {
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile)
	} else {
		home, err := os.UserHomeDir()
		cobra.CheckErr(err)

		viper.AddConfigPath(home)
		viper.AddConfigPath(".")
		viper.SetConfigType("yaml")
		viper.SetConfigName(".taskctl")
	}

	// Ler variaveis de ambiente com prefixo TASKCTL_
	viper.SetEnvPrefix("TASKCTL")
	viper.AutomaticEnv()

	// Valores padrao
	viper.SetDefault("format", "table")
	viper.SetDefault("storage.path", "tasks.json")
	viper.SetDefault("color", true)

	if err := viper.ReadInConfig(); err == nil {
		fmt.Fprintln(os.Stderr, "Usando config:", viper.ConfigFileUsed())
	}
}

Arquivo de configuracao ~/.taskctl.yaml:

format: table
color: true
storage:
  path: ~/tarefas.json
  backup: true
default_priority: media

Agora o usuario pode configurar via arquivo, variavel de ambiente (TASKCTL_FORMAT=json) ou flag (--format json). Viper resolve a precedencia automaticamente.

Shell Completion

Cobra gera shell completion automaticamente para Bash, Zsh, Fish e PowerShell:

var completionCmd = &cobra.Command{
	Use:   "completion [bash|zsh|fish|powershell]",
	Short: "Gera script de auto-complete 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.GenPowerShellCompletionWithDesc(os.Stdout)
		default:
			return fmt.Errorf("shell nao suportado: %s", args[0])
		}
	},
}

Para instalar:

# Bash
taskctl completion bash > /etc/bash_completion.d/taskctl

# Zsh
taskctl completion zsh > "${fpath[1]}/_taskctl"

Distribuindo com GoReleaser

GoReleaser automatiza builds multiplataforma e publicacao:

# .goreleaser.yaml
version: 2
builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64
    ldflags:
      - -s -w
      - -X main.version={{.Version}}
      - -X main.commit={{.Commit}}
      - -X main.date={{.Date}}

archives:
  - format: tar.gz
    name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
    format_overrides:
      - goos: windows
        format: zip

checksum:
  name_template: "checksums.txt"

changelog:
  sort: asc

Execute com:

goreleaser release --snapshot --clean

Isso gera binarios para Linux, macOS e Windows em amd64 e arm64 – seis binarios a partir de um unico comando.

Boas Praticas para CLIs em Go

  1. Use RunE, nao Run – retorne erros em vez de chamar os.Exit ou log.Fatal dentro dos comandos
  2. Ajuda clara – preencha Short, Long e Example em cada comando
  3. Exit codes – 0 para sucesso, 1 para erro do usuario, 2 para erro interno
  4. Stderr para logs, stdout para dados – permite piping (taskctl list | grep alta)
  5. Saida estruturada – oferca --format json para integracao com outros programas
  6. Versao – inclua --version com commit hash e data de build
  7. Cores opcionais – detecte se stdout e um terminal e respeite NO_COLOR

Para CLIs que fazem operacoes de rede, combine com context e timeouts para garantir que nenhuma operacao fique travada.

Exemplo Completo: Comando done

var doneCmd = &cobra.Command{
	Use:   "done [id]",
	Short: "Marca uma tarefa como concluida",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		id := 0
		if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil {
			return fmt.Errorf("ID invalido: %s", args[0])
		}

		tasks, err := loadTasks()
		if err != nil {
			return err
		}

		found := false
		for i := range tasks {
			if tasks[i].ID == id {
				tasks[i].Done = true
				found = true
				fmt.Printf("Tarefa #%d concluida: %s\n", id, tasks[i].Title)
				break
			}
		}

		if !found {
			return fmt.Errorf("tarefa #%d nao encontrada", id)
		}

		return saveTasks(tasks)
	},
}

FAQ

Cobra e Viper sao obrigatorios para CLIs em Go? Nao. Para CLIs simples com 1-2 flags, flag da standard library e suficiente. Cobra brilha quando voce tem subcomandos aninhados, shell completion e help automatico. Viper e necessario quando a configuracao vem de multiplas fontes.

Como testar comandos Cobra? Execute o comando programaticamente e capture a saida. Use bytes.Buffer como writer e cmd.SetOut() / cmd.SetErr() para redirecionar stdout/stderr. Para testes de integracao, compile o binario e execute com os/exec.

Qual a diferenca entre Cobra e urfave/cli? Cobra tem mais features (geracao de codigo, completion automatica, melhor documentacao). urfave/cli e mais leve e usa uma API baseada em structs. Na pratica, Cobra e o padrao de facto – kubectl, docker, gh, hugo, todos usam Cobra.

Como adicionar cores na saida? Use bibliotecas como fatih/color ou charmbracelet/lipgloss. Sempre verifique se o terminal suporta cores (os.Stdout e um TTY) e respeite a variavel de ambiente NO_COLOR.

Como distribuir CLIs Go para usuarios finais? GoReleaser para builds automatizados, Homebrew tap para macOS/Linux, Scoop bucket para Windows, e pacotes .deb/.rpm para distribuicoes Linux. Para integracao continua, use GitHub Actions ou Gitea Actions.

Se voce esta construindo microsservicos que precisam se comunicar, veja tambem nosso guia sobre gRPC em Go para entender como criar APIs de alta performance entre servicos. E para entender como Go se compara com outras linguagens para backend, confira Go vs Node.js e Go vs Python.