Go para CLI Tools: Guia Pratico
Introdução
Se você já usou Docker, Kubernetes, Terraform ou Hugo, usou uma ferramenta CLI escrita em Go. Isso não é coincidência. Go se consolidou como a linguagem dominante para construção de ferramentas de linha de comando, e os motivos vão muito além de "é rápido".
Neste guia, vou mostrar por que Go é uma escolha excelente para CLI tools, quais frameworks e bibliotecas usar, como estruturar seu projeto, e vamos construir exemplos práticos do zero. O foco é ser útil para desenvolvedores com pelo menos dois anos de experiência que querem expandir seu repertório para além do ecossistema JavaScript/TypeScript.
Uma nota importante: os exemplos de código neste artigo usam TypeScript como ponto de comparação, justamente para que você consiga entender os conceitos partindo de uma linguagem que provavelmente já conhece. Depois, implementamos a versão Go para você ver a diferença na prática.
Por que usar Go para ferramentas CLI
Compilação para binário único
Esse é o argumento matador. Quando você compila um programa Go, o resultado é um único binário estático. Sem runtime, sem dependências externas, sem node_modules. Você distribui um arquivo e ele simplesmente funciona.
Compare isso com uma CLI em Node.js: você precisa garantir que o usuário tem Node instalado, na versão certa, e muitas vezes precisa rodar npm install antes de usar. Em Go, você faz go build e entrega o executável.
Cross-compilation nativa
Go compila para qualquer combinação de sistema operacional e arquitetura com uma única mudança de variáveis de ambiente. Quer gerar binários para Linux, macOS e Windows a partir do seu Mac? Basta configurar GOOS e GOARCH. Sem ferramentas extras, sem containers de build.
Desempenho de startup instantâneo
Ferramentas CLI precisam iniciar rápido. Ninguém quer esperar 500ms toda vez que roda um comando. Go inicia em milissegundos porque não tem VM, não tem JIT, não tem interpretador. O binário é código de máquina puro.
Ecossistema maduro para CLI
A comunidade Go construiu bibliotecas de altíssima qualidade especificamente para CLI tools. Cobra, Viper, Bubble Tea, Lip Gloss: existe um ecossistema inteiro dedicado a criar experiências de terminal profissionais.
Setup: preparando o ambiente de desenvolvimento
Instalando Go
A instalação é direta. Acesse go.dev/dl e baixe o instalador para seu sistema. Depois, verifique:
go version
# go version go1.22.0 linux/amd64Inicializando o projeto
mkdir minha-cli && cd minha-cli
go mod init github.com/seu-usuario/minha-cliInstalando o Cobra (framework CLI principal)
go install github.com/spf13/cobra-cli@latest
cobra-cli initIsso gera a estrutura básica com main.go e o diretório cmd/ já configurado.
Exemplo prático: CLI para gerenciar tarefas
Vamos construir uma ferramenta CLI para gerenciar tarefas (to-do list) que persiste dados em um arquivo JSON local. Primeiro, vou mostrar como seria a lógica central em TypeScript para que o conceito fique claro. Depois, implementamos em Go.
A lógica de tarefas em TypeScript (referência conceitual)
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
interface Task {
id: number;
title: string;
done: boolean;
createdAt: string;
}
interface TaskStore {
tasks: Task[];
nextId: number;
}
const STORE_PATH = join(homedir(), ".tasks.json");
function loadStore(): TaskStore {
if (!existsSync(STORE_PATH)) {
return { tasks: [], nextId: 1 };
}
const raw = readFileSync(STORE_PATH, "utf-8");
return JSON.parse(raw) as TaskStore;
}
function saveStore(store: TaskStore): void {
writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf-8");
}
function addTask(title: string): Task {
const store = loadStore();
const task: Task = {
id: store.nextId,
title,
done: false,
createdAt: new Date().toISOString(),
};
store.tasks.push(task);
store.nextId += 1;
saveStore(store);
return task;
}
function listTasks(showAll: boolean): Task[] {
const store = loadStore();
if (showAll) {
return store.tasks;
}
return store.tasks.filter((t) => !t.done);
}
function completeTask(id: number): Task | null {
const store = loadStore();
const task = store.tasks.find((t) => t.id === id);
if (!task) return null;
task.done = true;
saveStore(store);
return task;
}
// Simulando o parse de argumentos da CLI
const command = process.argv[2];
const arg = process.argv[3];
switch (command) {
case "add":
if (!arg) {
console.error("Uso: tasks add <titulo>");
process.exit(1);
}
const newTask = addTask(arg);
console.log(`Tarefa #${newTask.id} criada: ${newTask.title}`);
break;
case "list":
const all = arg === "--all";
const tasks = listTasks(all);
tasks.forEach((t) => {
const status = t.done ? "[x]" : "[ ]";
console.log(`${status} #${t.id} ${t.title}`);
});
break;
case "done":
const completed = completeTask(Number(arg));
if (completed) {
console.log(`Tarefa #${completed.id} concluída!`);
} else {
console.error(`Tarefa #${arg} não encontrada.`);
}
break;
default:
console.log("Comandos: add, list, done");
}Funciona, mas perceba os problemas: o parse de argumentos é manual e frágil, não há help automático, não há validação robusta de flags, e para distribuir você precisa que o usuário tenha Node.js instalado.
A mesma CLI em Go com Cobra
Agora vamos ver como isso fica em Go usando o framework Cobra:
Um arquivo ausente inicia um armazenamento novo. Erros de leitura ou de decodificação interrompem o comando, preservando o arquivo existente para diagnóstico. Só informamos sucesso depois de uma gravação bem-sucedida. Se o comando falhar, a CLI termina com código diferente de zero.
// main.go
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"time"
"github.com/spf13/cobra"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
}
type TaskStore struct {
Tasks []Task `json:"tasks"`
NextID int `json:"next_id"`
}
func storePath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".tasks.json")
}
func loadStore() (TaskStore, error) {
path := storePath()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return TaskStore{NextID: 1}, nil
}
if err != nil {
return TaskStore{}, fmt.Errorf("ler %s: %w", path, err)
}
var store TaskStore
if err := json.Unmarshal(data, &store); err != nil {
return TaskStore{}, fmt.Errorf("JSON inválido em %s: %w", path, err)
}
if store.NextID < 1 {
return TaskStore{}, fmt.Errorf("next_id inválido em %s", path)
}
return store, nil
}
func saveStore(store TaskStore) error {
data, err := json.MarshalIndent(store, "", " ")
if err != nil {
return err
}
return os.WriteFile(storePath(), data, 0644)
}
func main() {
var showAll bool
rootCmd := &cobra.Command{
Use: "tasks",
Short: "Gerenciador de tarefas via terminal",
}
addCmd := &cobra.Command{
Use: "add [titulo]",
Short: "Adiciona uma nova tarefa",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
store, err := loadStore()
if err != nil {
return err
}
task := Task{
ID: store.NextID,
Title: args[0],
Done: false,
CreatedAt: time.Now(),
}
store.Tasks = append(store.Tasks, task)
store.NextID++
if err := saveStore(store); err != nil {
return fmt.Errorf("erro ao salvar: %w", err)
}
fmt.Printf("Tarefa #%d criada: %s\n", task.ID, task.Title)
return nil
},
}
listCmd := &cobra.Command{
Use: "list",
Short: "Lista tarefas pendentes",
RunE: func(cmd *cobra.Command, args []string) error {
store, err := loadStore()
if err != nil {
return err
}
for _, t := range store.Tasks {
if !showAll && t.Done {
continue
}
status := "[ ]"
if t.Done {
status = "[x]"
}
fmt.Printf("%s #%d %s\n", status, t.ID, t.Title)
}
return nil
},
}
listCmd.Flags().BoolVarP(&showAll, "all", "a", false, "Mostra todas as tarefas")
doneCmd := &cobra.Command{
Use: "done [id]",
Short: "Marca uma tarefa como concluída",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("ID inválido: %s", args[0])
}
store, err := loadStore()
if err != nil {
return err
}
for i := range store.Tasks {
if store.Tasks[i].ID == id {
store.Tasks[i].Done = true
if err := saveStore(store); err != nil {
return fmt.Errorf("erro ao salvar: %w", err)
}
fmt.Printf("Tarefa #%d concluída!\n", id)
return nil
}
}
return fmt.Errorf("tarefa #%d não encontrada", id)
},
}
rootCmd.AddCommand(addCmd, listCmd, doneCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}Compile e distribua:
go build -o tasks .
./tasks add "Escrever artigo sobre Go CLI"
./tasks list
./tasks done 1
./tasks list --allO resultado é um binário de aproximadamente 7MB que roda em qualquer máquina sem dependências. Rode ./tasks --help e o Cobra gera automaticamente uma documentação completa dos comandos e flags.
Validação de configuração em TypeScript como referência conceitual
Vamos imaginar que você precisa de uma ferramenta que valida arquivos de configuração JSON de um projeto. Novamente, primeiro a referência em TypeScript para clareza conceitual:
import { readFileSync } from "fs";
import { z } from "zod";
const ConfigSchema = z.object({
appName: z.string().min(1, "appName é obrigatório"),
port: z.number().int().min(1024).max(65535),
database: z.object({
host: z.string().min(1),
port: z.number().int(),
name: z.string().min(1),
}),
features: z.record(z.boolean()).optional(),
});
type Config = z.infer<typeof ConfigSchema>;
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
function validateConfig(filePath: string): ValidationResult {
const result: ValidationResult = {
valid: true,
errors: [],
warnings: [],
};
let raw: string;
try {
raw = readFileSync(filePath, "utf-8");
} catch {
return { valid: false, errors: [`Arquivo não encontrado: ${filePath}`], warnings: [] };
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { valid: false, errors: ["JSON inválido"], warnings: [] };
}
const parseResult = ConfigSchema.safeParse(parsed);
if (!parseResult.success) {
result.valid = false;
result.errors = parseResult.error.issues.map(
(issue) => `${issue.path.join(".")}: ${issue.message}`
);
return result;
}
const config = parseResult.data;
if (config.port === 3000) {
result.warnings.push("Porta 3000 é comum em desenvolvimento. Considere mudar para produção.");
}
if (!config.features || Object.keys(config.features).length === 0) {
result.warnings.push("Nenhuma feature flag configurada.");
}
return result;
}
const filePath = process.argv[2] || "config.json";
const validation = validateConfig(filePath);
if (validation.valid) {
console.log("Configuração válida!");
} else {
console.error("Erros encontrados:");
validation.errors.forEach((e) => console.error(` - ${e}`));
}
if (validation.warnings.length > 0) {
console.warn("Avisos:");
validation.warnings.forEach((w) => console.warn(` - ${w}`));
}
process.exit(validation.valid ? 0 : 1);Este bloco demonstra as regras em TypeScript. Uma implementação equivalente em Go fica fora deste exemplo.
Boas práticas para CLI tools em Go
Estrutura de diretórios recomendada
Para projetos maiores, siga o padrão que projetos como kubectl e gh (GitHub CLI) utilizam:
minha-cli/
cmd/
root.go
add.go
list.go
done.go
internal/
store/
store.go
config/
config.go
pkg/
formatter/
table.go
main.go
go.mod
go.sumO diretório cmd/ contém a definição dos comandos Cobra. O diretório internal/ contém a lógica de negócio que não deve ser importada por outros projetos. O diretório pkg/ contém código reutilizável que pode ser compartilhado.
Use códigos de saída corretamente
Uma CLI profissional usa exit codes para comunicar resultado. Zero significa sucesso, um significa erro genérico, e você pode definir códigos específicos para situações distintas. Isso permite que scripts e pipelines de CI reajam adequadamente.
Trate sinais do sistema operacional
Ferramentas que fazem operações longas (downloads, processamento de arquivos grandes) devem capturar SIGINT e SIGTERM para fazer cleanup gracefully. Go torna isso simples com o pacote os/signal.
Saída estruturada com flags
Ofereça uma flag --output ou --format que permita alternar entre saída humana (com cores e formatação) e saída para máquina (JSON, YAML).
Guias Relacionados
Explore mais guias técnicos
Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.
Ver todos os guias