---
title: "Cobra em Go: CLI de Produção com Subcomandos, Flags e Exit Codes"
url: "https://golang.com.br/blog/cobra-go-cli-producao/"
markdown_url: "https://golang.com.br/blog/cobra-go-cli-producao.MD"
description: "Cobra em Go na prática: root command, subcomandos, flags persistentes, Viper, exit codes, saída JSON, context, testes e completions para CLI de produção."
date: "2026-07-29"
author: "Golang Brasil"
---

# Cobra em Go: CLI de Produção com Subcomandos, Flags e Exit Codes

Cobra em Go na prática: root command, subcomandos, flags persistentes, Viper, exit codes, saída JSON, context, testes e completions para CLI de produção.


Se a pergunta é **como criar uma CLI profissional em Go em 2026**, a resposta mais citada por times brasileiros de plataforma, SRE e backend é **Cobra**: árvore de comandos clara, help automático, flags persistentes, completions e um contrato de saída que scripts e CI conseguem confiar. O pacote `flag` da biblioteca padrão resolve o protótipo de um comando só. Quando a ferramenta ganha `serve`, `migrate`, `version`, `--json` e configuração por ambiente, Cobra deixa de ser “mais uma dependência” e vira o esqueleto do produto.

Este guia é o deep dive de **produção** em cima de Cobra: contrato de exit codes, stdout versus stderr, saída JSON estável, `context` com cancelamento, integração com [Viper](/blog/configuracao-go-viper-variaveis-ambiente/), testes sem flakiness e o caminho até [GoReleaser](/blog/goreleaser-go-binarios-checksums-sbom/). Para o passo a passo introdutório (flag nativo, primeiro `root.go`, exemplo de backup), use o [tutorial de CLI tools em Go](/tutoriais/go-cli-tools/). Se a interface for interativa no terminal, compare com [Bubble Tea](/blog/bubble-tea-go-tui-terminal/) — Cobra e TUI resolvem problemas diferentes.

## Resposta rápida: Cobra ou `flag`?

| Cenário | Escolha |
|---|---|
| Um binário, 2–3 flags, sem subcomando | `flag` da stdlib |
| Subcomandos (`serve`, `migrate`, `completion`) | **Cobra** |
| Flags globais (`--config`, `--verbose`) + locais | **Cobra** (persistent + local) |
| Completions bash/zsh e help gerado | **Cobra** |
| UI interativa (listas, spinners, painéis) | [Bubble Tea](/blog/bubble-tea-go-tui-terminal/) (Cobra pode só lançar o modo TUI) |
| Distribuir binário versionado multi-OS | Cobra + [GoReleaser](/blog/goreleaser-go-binarios-checksums-sbom/) |

**Regra prática:** se o README já lista mais de um verbo na linha de comando, comece com Cobra. Refatorar de `flag` para Cobra no meio do caminho dói mais do que adotar cedo.

## O modelo mental do Cobra

Cobra modela a CLI como uma **árvore de `*cobra.Command`**:

- cada nó tem `Use`, `Short`, `Long`, `Run`/`RunE`;
- flags locais valem só naquele comando;
- flags persistentes descem para os filhos;
- `Execute()` parseia `os.Args`, escolhe o nó e roda o handler.

```go
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

func main() {
	root := &cobra.Command{
		Use:   "jobctl",
		Short: "Controla jobs de processamento em lote",
	}

	var verbose bool
	root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "log detalhado em stderr")

	versionCmd := &cobra.Command{
		Use:   "version",
		Short: "Imprime a versão do binário",
		RunE: func(cmd *cobra.Command, args []string) error {
			fmt.Fprintln(cmd.OutOrStdout(), "jobctl 1.4.2")
			return nil
		},
	}

	root.AddCommand(versionCmd)

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

Três detalhes que separam CLI de brinquedo de CLI de produção:

1. **`RunE` em vez de `Run`** — erros sobem para o `Execute` e você controla o exit code.
2. **`cmd.OutOrStdout()` / `cmd.ErrOrStderr()`** — permitem redirecionar saída em testes sem tocar em `os.Stdout` global.
3. **Lógica de negócio fora do `RunE`** — o comando só traduz flags em parâmetros; a função de domínio recebe `context.Context` e interfaces.

## Estrutura de projeto que escala

Para ferramentas que vão viver anos (CLIs internas de fintech, operadores de plataforma, geradores de código), evite colocar tudo em `main.go`:

```text
cmd/
  jobctl/
    main.go          # só monta root e chama Execute
internal/
  cli/
    root.go          # NewRoot() *cobra.Command
    serve.go
    migrate.go
    version.go
  app/
    server.go        # lógica real
    migrator.go
```

`main` fica fino:

```go
package main

import (
	"os"

	"example.com/jobctl/internal/cli"
)

func main() {
	if err := cli.NewRoot().Execute(); err != nil {
		os.Exit(1)
	}
}
```

Essa separação combina com [Clean Architecture sem overengineering](/blog/clean-architecture-go-sem-overengineering/) e com [dependency injection sem framework](/blog/dependency-injection-go-sem-framework/): o `RunE` resolve dependências (logger, config, cliente HTTP) e chama o caso de uso.

## Subcomandos e flags persistentes

Flags persistentes no root (`--config`, `--verbose`, `--output`) evitam repetir a mesma opção em cada filho. Flags locais ficam no comando que as consome (`--port` em `serve`, `--dry-run` em `migrate`).

```go
func NewRoot() *cobra.Command {
	var (
		cfgPath string
		output  string
	)

	root := &cobra.Command{
		Use:           "jobctl",
		Short:         "Controla jobs de processamento em lote",
		SilenceUsage:  true,  // não despeja help em erro de runtime
		SilenceErrors: true,  // você imprime o erro do seu jeito
	}

	root.PersistentFlags().StringVar(&cfgPath, "config", "", "arquivo de configuração")
	root.PersistentFlags().StringVarP(&output, "output", "o", "text", "text|json")

	root.AddCommand(newServeCmd(&cfgPath, &output))
	root.AddCommand(newMigrateCmd(&cfgPath, &output))
	root.AddCommand(newVersionCmd())
	return root
}

func newServeCmd(cfgPath, output *string) *cobra.Command {
	var port int
	cmd := &cobra.Command{
		Use:   "serve",
		Short: "Sobe o servidor HTTP de jobs",
		RunE: func(cmd *cobra.Command, args []string) error {
			ctx := cmd.Context()
			cfg, err := loadConfig(*cfgPath)
			if err != nil {
				return err
			}
			if port != 0 {
				cfg.Port = port
			}
			return app.RunServer(ctx, cfg, *output)
		},
	}
	cmd.Flags().IntVar(&port, "port", 0, "porta HTTP (sobrescreve config)")
	return cmd
}
```

`SilenceUsage: true` é o detalhe que a maioria esquece: em produção, um erro de banco não deve imprimir duas páginas de help. Help é para uso incorreto; erro de runtime vai para stderr com mensagem curta.

## Contrato de saída: stdout, stderr e exit codes

Scripts, pipelines e GitHub Actions não leem a sua intenção — leem **bytes e código de saída**.

| Stream | Conteúdo |
|---|---|
| **stdout** | Resultado consumível: JSON, tabela, ID criado, versão |
| **stderr** | Log humano, progresso, avisos, stack resumida |
| **exit 0** | Sucesso |
| **exit 2** | Uso incorreto (flag inválida, args faltando) |
| **exit 1** | Falha de execução (rede, banco, regra de negócio) |

```go
func writeResult(cmd *cobra.Command, output string, v any) error {
	switch output {
	case "json":
		enc := json.NewEncoder(cmd.OutOrStdout())
		enc.SetIndent("", "  ")
		return enc.Encode(v)
	case "text":
		_, err := fmt.Fprintln(cmd.OutOrStdout(), v)
		return err
	default:
		return fmt.Errorf("output %q inválido (use text|json)", output)
	}
}
```

Nunca misture log estruturado em stdout se a saída for pipeada para `jq`. Use [slog](/blog/slog-go-logging-estruturado/) apontando para stderr, ou um nível `debug` só com `--verbose`.

Para mapear erros de validação de flag para exit 2, trate a função de saída:

```go
func Execute() int {
	root := NewRoot()
	if err := root.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, "erro:", err)
		if errors.Is(err, errUsage) {
			return 2
		}
		return 1
	}
	return 0
}

func main() { os.Exit(Execute()) }
```

## `context`: Ctrl+C, timeout e cancelamento

CLIs longas (`migrate`, `export`, `serve`) precisam reagir a `SIGINT`/`SIGTERM`. O Cobra moderno propaga `cmd.Context()`; combine com [graceful shutdown](/blog/graceful-shutdown-go-producao/) e [context com timeout](/blog/context-timeout-cancelamento-go/):

```go
root.PersistentPreRun = func(cmd *cobra.Command, args []string) {
	// cmd.Context() já cancela em sinais quando configurado via CommandContext
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	root := cli.NewRoot()
	if err := root.ExecuteContext(ctx); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
```

Dentro do `RunE`, passe `cmd.Context()` para HTTP clients, migrations e workers. Assim um Ctrl+C cancela a operação em andamento em vez de deixar goroutine órfã — o mesmo espírito de [errgroup e cancelamento](/blog/errgroup-go-concorrencia-cancelamento-erros/).

## Cobra + Viper: uma fonte de verdade

O casamento clássico do ecossistema:

1. Cobra declara a flag;
2. Viper bind a flag e o env;
3. A aplicação lê só do Viper (ou de uma struct preenchida uma vez no boot).

```go
func bindConfig(cmd *cobra.Command) error {
	v := viper.New()
	v.SetEnvPrefix("JOBCTL")
	v.AutomaticEnv()

	if cfg := cmd.Flags().Lookup("config"); cfg != nil && cfg.Value.String() != "" {
		v.SetConfigFile(cfg.Value.String())
		if err := v.ReadInConfig(); err != nil {
			return err
		}
	}
	if err := v.BindPFlag("port", cmd.Flags().Lookup("port")); err != nil {
		return err
	}
	return nil
}
```

Precedência que o time precisa documentar no README (da mais forte para a mais fraca):

1. flag na linha de comando;
2. variável de ambiente;
3. arquivo de configuração;
4. default no código.

Detalhes e armadilhas de secrets estão no [guia de configuração com Viper](/blog/configuracao-go-viper-variaveis-ambiente/).

## Completions e help que vendem a ferramenta

Completions não são “nice to have”: em times grandes, a CLI que completa subcomandos e valores de `--output` é a que as pessoas realmente usam.

```go
root.AddCommand(&cobra.Command{
	Use:   "completion [bash|zsh|fish|powershell]",
	Short: "Gera script de autocomplete",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		switch args[0] {
		case "bash":
			return cmd.Root().GenBashCompletion(cmd.OutOrStdout())
		case "zsh":
			return cmd.Root().GenZshCompletion(cmd.OutOrStdout())
		case "fish":
			return cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true)
		case "powershell":
			return cmd.Root().GenPowerShellCompletionWithDesc(cmd.OutOrStdout())
		default:
			return fmt.Errorf("shell %q não suportado", args[0])
		}
	},
})
```

Documente no README:

```bash
# zsh
jobctl completion zsh > "${fpath[1]}/_jobctl"
```

`Short` e `Long` bem escritos são documentação barata: o help gerado vira a referência diária do time. Mantenha exemplos reais em `Example`:

```go
Example: `  jobctl serve --port 8080
  jobctl migrate --dry-run
  jobctl version --output json`,
```

## Testes: ExecuteC, buffers e lógica extraída

Não suba servidor de verdade no teste do parser. O padrão idiomático:

```go
func TestVersionJSON(t *testing.T) {
	root := cli.NewRoot()
	buf := new(bytes.Buffer)
	errBuf := new(bytes.Buffer)
	root.SetOut(buf)
	root.SetErr(errBuf)
	root.SetArgs([]string{"version", "--output", "json"})

	_, err := root.ExecuteC()
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Contains(buf.Bytes(), []byte(`"version"`)) {
		t.Fatalf("stdout = %s", buf.String())
	}
	if errBuf.Len() != 0 {
		t.Fatalf("stderr inesperado: %s", errBuf.String())
	}
}
```

Para regras de negócio, teste a função `app.RunServer` com [table-driven tests](/blog/testes-tabela-go-guia-table-driven-tests/) e [mocks/fakes](/blog/mocks-go-testify-gomock-fakes-httptest/). O comando Cobra vira uma casca fina — barata de testar e difícil de quebrar.

## Versionamento e release do binário

Toda CLI de produção responde `version` com commit e data de build:

```go
var (
	version = "dev"
	commit  = "none"
	date    = "unknown"
)

// ldflags no build:
// -X example.com/jobctl/internal/cli.version=1.4.2
```

O [GoReleaser](/blog/goreleaser-go-binarios-checksums-sbom/) preenche esses campos, gera checksums, SBOM e binários para linux/darwin/windows. Sem isso, o suporte vira “qual binário você baixou do Slack?”. Combine com [govulncheck](/blog/govulncheck-go-vulnerabilidades-dependencias/) no CI antes de publicar a release.

## Armadilhas comuns em CLI Go

1. **Log em stdout com `--output json`** — quebra o pipe. stderr para humano, stdout para máquina.
2. **`os.Exit` no meio do `RunE`** — impede defers, flush de logs e testes. Retorne `error` e saia só no `main`.
3. **Flags obrigatórias sem `MarkFlagRequired`** — o usuário descobre o erro tarde. Use `cmd.MarkFlagRequired("file")` ou valide no `PreRunE`.
4. **Globals mutáveis para flags** — dificulta teste paralelo. Prefira closures ou structs de opções por comando.
5. **Ignorar `context` em `serve`** — Ctrl+C deixa conexões abertas. Veja [graceful shutdown](/blog/graceful-shutdown-go-producao/).
6. **Misturar TUI e flags sem modo scriptável** — se a ferramenta entra em menu interativo sem `stdin` TTY, pipelines morrem. Ofereça caminho não interativo (Cobra puro) e opcionalmente um subcomando `tui` com [Bubble Tea](/blog/bubble-tea-go-tui-terminal/).
7. **Help em erro de rede** — configure `SilenceUsage` e mensagens curtas; detalhe fica no log com `--verbose`.

## Cobra no dia a dia do mercado brasileiro

Em vagas de Go no Brasil — plataformas de pagamento, fintechs, e-commerce, cloud providers — é comum o time manter CLIs internas: provisionar tenant, reprocessar fila, emitir token de debug, rodar migration com guardrail. Dominar Cobra aparece em entrevistas de backend sênior como “como você estruturaria a ferramenta X?”. Monte no portfólio um `jobctl` pequeno com `serve`, `migrate`, `version --output json`, completions e release via GoReleaser; isso conversa bem com o [guia de projetos para portfólio](/blog/projetos-go-para-portfolio-ideias-primeira-vaga/) e com as [vagas listadas no site](/vagas/).

Se você ainda está no começo da linguagem, encaixe CLI depois de [módulos](/aprenda/go-modules-na-pratica/) e de um [HTTP server básico](/aprenda/api-rest-go/) — a mesma disciplina de `context`, erros e testes se aplica.

## Perguntas frequentes

### O que é Cobra em Go?

Cobra é a biblioteca mais usada para construir CLIs em Go. Ela organiza comandos e subcomandos, gera help automático, aceita flags locais e persistentes, expõe completions para bash/zsh/fish/powershell e integra bem com Viper para configuração. Projetos como kubectl, Hugo e GitHub CLI popularizaram esse modelo de árvore de comandos.

### Quando usar Cobra em vez do pacote flag?

Use o pacote `flag` da biblioteca padrão para uma ferramenta de um único comando com poucas opções. Passe para Cobra quando precisar de subcomandos (`serve`, `migrate`, `version`), help consistente, flags compartilhadas, completions e uma árvore de comandos que cresce com o produto. O [tutorial introdutório de CLI](/tutoriais/go-cli-tools/) mostra a progressão natural entre os dois.

### Como Cobra e Viper trabalham juntos?

Cobra cuida da interface de linha de comando; Viper cuida da configuração. O padrão comum é bindar flags do Cobra no Viper e deixar o Viper resolver a precedência entre flag, variável de ambiente e arquivo YAML/JSON. Assim o mesmo valor pode vir de `--port`, `PORT` ou `config.yaml`. Veja o [guia de configuração em Go](/blog/configuracao-go-viper-variaveis-ambiente/).

### Qual exit code uma CLI Go deve devolver?

Use `0` para sucesso. Use códigos diferentes de zero para falha: `1` para erro de negócio ou runtime, `2` para uso incorreto (flags inválidas, argumentos faltando). Em scripts e CI, o exit code é o contrato — nunca imprima o erro e saia com `0`.

### Como testar comandos Cobra?

Monte o `*cobra.Command` em teste, redirecione `Out` e `Err` para buffers, chame `ExecuteC` e assert sobre stdout, stderr e erro retornado. Extraia a lógica de negócio para funções que recebem `context` e dependências, para não acoplar testes ao parsing de flags. Combine com [testes em tabela](/blog/testes-tabela-go-guia-table-driven-tests/).

## Conclusão

Cobra é o padrão de fato para CLI em Go porque impõe uma árvore de comandos legível, help previsível e extensão barata. Em produção, o diferencial não é “usar Cobra” — é respeitar o contrato: `RunE`, stdout limpo, stderr para log, exit codes corretos, `ExecuteContext` com sinais, Viper para config, testes com buffers e release reproduzível com GoReleaser.

Próximos passos na malha de ferramentas do site:

- [Tutorial: CLI tools com flag e Cobra](/tutoriais/go-cli-tools/)
- [Configuração: Viper e variáveis de ambiente](/blog/configuracao-go-viper-variaveis-ambiente/)
- [GoReleaser: binários, checksums e SBOM](/blog/goreleaser-go-binarios-checksums-sbom/)
- [Bubble Tea: TUI interativa no terminal](/blog/bubble-tea-go-tui-terminal/)
- [Graceful shutdown em produção](/blog/graceful-shutdown-go-producao/)
- [slog: logging estruturado](/blog/slog-go-logging-estruturado/)
- [Testes em tabela](/blog/testes-tabela-go-guia-table-driven-tests/)
- [Projetos Go para portfólio](/blog/projetos-go-para-portfolio-ideias-primeira-vaga/)
- [Vagas de Go no Brasil](/vagas/)

Implemente um `jobctl` mínimo esta semana: `version`, `serve` com shutdown limpo e `migrate --dry-run`. Essa base cobre a maior parte do que entrevistas e times de plataforma esperam de uma CLI Go séria.
