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 installounpm install - Compilacao cruzada –
GOOS=windows GOARCH=amd64 go buildgera um.exea partir do Linux - Startup instantaneo – CLIs Go iniciam em milissegundos, nao segundos
- Concorrencia nativa – goroutines para operacoes paralelas (downloads, builds, deploys)
- Standard library poderosa –
flag,os,io,filepathcobrem 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:
- Flags da linha de comando (maior prioridade)
- Variaveis de ambiente
- Arquivo de configuracao (YAML, JSON, TOML)
- 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
- Use
RunE, naoRun– retorne erros em vez de chamaros.Exitoulog.Fataldentro dos comandos - Ajuda clara – preencha
Short,LongeExampleem cada comando - Exit codes – 0 para sucesso, 1 para erro do usuario, 2 para erro interno
- Stderr para logs, stdout para dados – permite piping (
taskctl list | grep alta) - Saida estruturada – oferca
--format jsonpara integracao com outros programas - Versao – inclua
--versioncom commit hash e data de build - 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.