Ferramentas de linha de comando (CLI) são essenciais no arsenal de qualquer desenvolvedor. Desde gerenciadores de pacotes como npm e pip até infraestrutura como kubectl e terraform, CLIs bem projetadas aumentam a produtividade exponencialmente. Go é a linguagem perfeita para criar CLIs rápidas, portáteis e eficientes.

Neste guia completo, você aprenderá a construir CLI tools profissionais usando Go, desde flags simples até ferramentas complexas com subcomandos e auto-complete.

Por Que Go para CLI Tools?

Vantagens Competitivas

┌─────────────────────────────────────────────────────────────────┐
│  CARACTERÍSTICA     │  GO    │  PYTHON  │  NODE.JS  │  RUST   │
├─────────────────────────────────────────────────────────────────┤
│  Startup            │  ~5ms  │  ~100ms  │  ~200ms   │  ~10ms  │
├─────────────────────────────────────────────────────────────────┤
│  Binário único      │  ✅    │  ❌      │  ❌       │  ✅     │
├─────────────────────────────────────────────────────────────────┤
│  Cross-compile      │  ✅    │  ❌      │  ❌       │  ✅     │
├─────────────────────────────────────────────────────────────────┤
│  Memória            │  Baixa │  Alta    │  Média    │  Baixa  │
├─────────────────────────────────────────────────────────────────┤
│  Curva de aprend.   │  Média │  Baixa   │  Baixa    │  Alta   │
└─────────────────────────────────────────────────────────────────┘

Casos de Sucesso

  • Docker: Container runtime escrito em Go
  • Kubernetes: Orquestração de containers
  • Hugo: Gerador de sites estáticos
  • Terraform: Infrastructure as Code
  • Cobra: Framework CLI usado por Kubernetes, etcd, e muitos outros

Fundamentos de CLI em Go

Flags Nativas com flag Package

Go inclui um pacote flag na biblioteca padrão para parsing de argumentos:

// cmd/simple-cli/main.go
package main

import (
	"flag"
	"fmt"
	"os"
)

type Config struct {
	Name    string
	Verbose bool
	Count   int
	Output  string
}

func main() {
	config := parseFlags()
	
	if config.Verbose {
		fmt.Println("Modo verbose ativado")
	}
	
	fmt.Printf("Olá, %s! Contagem: %d\n", config.Name, config.Count)
	
	if config.Output != "" {
		fmt.Printf("Saída será salva em: %s\n", config.Output)
	}
}

func parseFlags() Config {
	var config Config
	
	// Definir flags
	flag.StringVar(&config.Name, "name", "Mundo", "Nome para saudar")
	flag.BoolVar(&config.Verbose, "verbose", false, "Modo detalhado")
	flag.IntVar(&config.Count, "count", 1, "Número de saudações")
	flag.StringVar(&config.Output, "output", "", "Arquivo de saída (opcional)")
	
	// Parse
	flag.Parse()
	
	return config
}

Executando

# Compilar
go build -o mycli cmd/simple-cli/main.go

# Uso básico
./mycli -name="Go" -count=3
# Output: Olá, Go! Contagem: 3

# Modo verbose
./mycli -verbose -name="Desenvolvedor"
# Output:
# Modo verbose ativado
# Olá, Desenvolvedor! Contagem: 1

# Ajuda automática
./mycli -help
# Usage of ./mycli:
#   -count int
#       Número de saudações (default 1)
#   -name string
#       Nome para saudar (default "Mundo")
#   -output string
#       Arquivo de saída (opcional)
#   -verbose
#       Modo detalhado

Limitações do Pacote flag

O pacote flag é funcional, mas limitado para CLIs complexos:

  • ❌ Sem subcomandos nativos
  • ❌ Sem auto-complete
  • ❌ Sem documentação automática
  • ❌ Sintaxe de flags limitada (apenas -flag ou --flag)
  • ❌ Sem validação de argumentos integrada

Para CLIs profissionais, usamos o Cobra.

Cobra: O Framework CLI Definitivo

Instalação e Setup

# Instalar CLI do Cobra
go install github.com/spf13/cobra-cli@latest

# Inicializar projeto
mkdir myapp && cd myapp
cobra-cli init --pkg-name myapp

# Estrutura criada:
# myapp/
# ├── cmd/
# │   └── root.go
# ├── main.go
# ├── go.mod
# └── LICENSE

Estrutura do Projeto Cobra

myapp/
├── cmd/                    # Comandos da aplicação
│   ├── root.go            # Comando raiz
│   ├── serve.go           # Subcomando: serve
│   ├── config.go          # Subcomando: config
│   └── version.go         # Subcomando: version
├── internal/              # Código interno
│   ├── config/
│   │   └── config.go
│   └── server/
│       └── server.go
├── pkg/                   # Bibliotecas reutilizáveis
│   └── utils/
│       └── utils.go
├── main.go
└── go.mod

Comando Raiz (root.go)

// cmd/root.go
package cmd

import (
	"fmt"
	"os"

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

var cfgFile string

// rootCmd representa o comando base
var rootCmd = &cobra.Command{
	Use:   "myapp",
	Short: "Uma aplicação CLI de exemplo",
	Long: `MyApp é uma ferramenta de linha de comando que demonstra
as melhores práticas para construir CLIs profissionais em Go.

Complete documentation is available at http://example.com`,
	// Executado antes de qualquer subcomando
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		// Inicialização global
		fmt.Println("Inicializando aplicação...")
	},
	// Executado quando nenhum subcomando é especificado
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Use 'myapp --help' para ver comandos disponíveis")
	},
}

// Execute adiciona todos os subcomandos
func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	// Inicialização antes de parsing de flags
	cobra.OnInitialize(initConfig)

	// Flags persistentes (disponíveis em todos os subcomandos)
	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", 
		"arquivo de config (default é $HOME/.myapp.yaml)")
	
	// Flags locais (apenas para comando raiz)
	rootCmd.Flags().BoolP("toggle", "t", false, 
		"Help message for toggle")

	// Viper binding (para ler de arquivo de config)
	viper.BindPFlag("toggle", rootCmd.Flags().Lookup("toggle"))
}

// initConfig lê arquivo de configuração
func initConfig() {
	if cfgFile != "" {
		viper.SetConfigFile(cfgFile)
	} else {
		home, err := os.UserHomeDir()
		cobra.CheckErr(err)

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

	viper.AutomaticEnv()

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

Subcomandos

Criando Subcomandos

# Gerar novo subcomando
cobra-cli add serve
cobra-cli add config
cobra-cli add version

Exemplo: Subcomando serve

// cmd/serve.go
package cmd

import (
	"fmt"
	"log"
	"net/http"

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

// Flags específicas do comando serve
var (
	port    int
	host    string
	env     string
)

// serveCmd representa o comando serve
var serveCmd = &cobra.Command{
	Use:   "serve",
	Short: "Inicia o servidor HTTP",
	Long: `Inicia o servidor HTTP com as configurações especificadas.

Exemplos:
  # Servidor padrão
  myapp serve

  # Servidor em porta específica
  myapp serve --port=8080

  # Modo desenvolvimento
  myapp serve --env=development --verbose`,
	
	// Validação de argumentos
	Args: cobra.NoArgs, // Não aceita argumentos posicionais
	
	// Execução principal
	Run: func(cmd *cobra.Command, args []string) {
		startServer()
	},
}

func init() {
	rootCmd.AddCommand(serveCmd)

	// Definir flags
	serveCmd.Flags().IntVarP(&port, "port", "p", 8080, 
		"Porta do servidor")
	serveCmd.Flags().StringVar(&host, "host", "localhost", 
		"Host do servidor")
	serveCmd.Flags().StringVarP(&env, "env", "e", "production", 
		"Ambiente (development|staging|production)")

	// Viper binding para ler de config file também
	viper.BindPFlag("serve.port", serveCmd.Flags().Lookup("port"))
	viper.BindPFlag("serve.host", serveCmd.Flags().Lookup("host"))
	viper.BindPFlag("serve.env", serveCmd.Flags().Lookup("env"))
}

func startServer() {
	addr := fmt.Sprintf("%s:%d", host, port)
	
	fmt.Printf("🚀 Iniciando servidor em %s (%s)\n", addr, env)
	
	if env == "development" {
		fmt.Println("📋 Modo desenvolvimento ativado")
		fmt.Println("   - Hot reload habilitado")
		fmt.Println("   - Logs detalhados")
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "MyApp Server - %s\n", env)
	})
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(map[string]string{
			"status": "healthy",
			"env":    env,
		})
	})

	log.Printf("Servidor rodando em http://%s", addr)
	log.Fatal(http.ListenAndServe(addr, mux))
}

Exemplo: Subcomando config

// cmd/config.go
package cmd

import (
	"fmt"

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

var configCmd = &cobra.Command{
	Use:   "config",
	Short: "Gerencia configurações da aplicação",
}

var configGetCmd = &cobra.Command{
	Use:   "get [chave]",
	Short: "Obtém valor de uma configuração",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		key := args[0]
		value := viper.Get(key)
		if value == nil {
			fmt.Printf("Configuração '%s' não encontrada\n", key)
			return
		}
		fmt.Printf("%s: %v\n", key, value)
	},
}

var configSetCmd = &cobra.Command{
	Use:   "set [chave] [valor]",
	Short: "Define valor de uma configuração",
	Args:  cobra.ExactArgs(2),
	Run: func(cmd *cobra.Command, args []string) {
		key, value := args[0], args[1]
		viper.Set(key, value)
		
		if err := viper.WriteConfig(); err != nil {
			// Criar config se não existir
			viper.SafeWriteConfig()
		}
		
		fmt.Printf("✅ %s = %s\n", key, value)
	},
}

var configListCmd = &cobra.Command{
	Use:   "list",
	Short: "Lista todas as configurações",
	Run: func(cmd *cobra.Command, args []string) {
		settings := viper.AllSettings()
		if len(settings) == 0 {
			fmt.Println("Nenhuma configuração definida")
			return
		}
		
		fmt.Println("Configurações atuais:")
		for k, v := range settings {
			fmt.Printf("  %s: %v\n", k, v)
		}
	},
}

func init() {
	rootCmd.AddCommand(configCmd)
	
	// Adicionar sub-subcomandos
	configCmd.AddCommand(configGetCmd)
	configCmd.AddCommand(configSetCmd)
	configCmd.AddCommand(configListCmd)
}

Validação de Argumentos

Tipos de Validação

// Sem argumentos
cobra.NoArgs

// N argumentos exatos
cobra.ExactArgs(2)

// Range de argumentos
cobra.RangeArgs(1, 3)

// Mínimo de argumentos
cobra.MinimumNArgs(1)

// Máximo de argumentos
cobra.MaximumNArgs(3)

// Argumentos arbitrários (default)
cobra.ArbitraryArgs

// Validação customizada
func(cmd *cobra.Command, args []string) error {
    if len(args) != 2 {
        return fmt.Errorf("requer exatamente 2 argumentos, recebeu %d", len(args))
    }
    return nil
}

Validação Customizada

// cmd/user.go
package cmd

import (
	"fmt"
	"regexp"

	"github.com/spf13/cobra"
)

var userCmd = &cobra.Command{
	Use:   "user [email]",
	Short: "Busca usuário por email",
	Args:  validateEmail,
	Run: func(cmd *cobra.Command, args []string) {
		email := args[0]
		fmt.Printf("Buscando usuário: %s\n", email)
		// ...
	},
}

func validateEmail(cmd *cobra.Command, args []string) error {
	if len(args) != 1 {
		return fmt.Errorf("uso: myapp user [email]")
	}
	
	email := args[0]
	emailRegex := regexp.MustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)
	
	if !emailRegex.MatchString(email) {
		return fmt.Errorf("'%s' não é um email válido", email)
	}
	
	return nil
}

Arquivos de Configuração com Viper

Formatos Suportados

# $HOME/.myapp.yaml
app:
  name: "MyApp"
  version: "1.0.0"

server:
  host: "0.0.0.0"
  port: 8080
  timeout: 30

database:
  host: "localhost"
  port: 5432
  name: "myapp"
  ssl_mode: "disable"

features:
  - auth
  - logging
  - metrics
// .myapp.json
{
  "server": {
    "port": 8080,
    "host": "localhost"
  },
  "debug": true
}

Uso no Código

package config

import (
	"github.com/spf13/viper"
)

type Config struct {
	App      AppConfig
	Server   ServerConfig
	Database DatabaseConfig
}

type AppConfig struct {
	Name    string
	Version string
}

type ServerConfig struct {
	Host    string
	Port    int
	Timeout int
}

type DatabaseConfig struct {
	Host    string
	Port    int
	Name    string
	SSLMode string
}

func Load() (*Config, error) {
	var cfg Config
	
	if err := viper.Unmarshal(&cfg); err != nil {
		return nil, err
	}
	
	return &cfg, nil
}

// Uso
func main() {
	cfg, err := config.Load()
	if err != nil {
		log.Fatal(err)
	}
	
	fmt.Printf("Servidor: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
}

Exemplo Real: Ferramenta de Backup

Vamos criar uma CLI completa para backups:

// cmd/backup/main.go
package main

import "backup/cmd"

func main() {
	cmd.Execute()
}
// cmd/root.go
package cmd

import (
	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "backup",
	Short: "Ferramenta de backup automático",
	Long: `Backup CLI é uma ferramenta poderosa para realizar backups
automáticos de arquivos e bancos de dados.

Suporta:
  - Backups locais e remotos (S3, GCS, Azure)
  - Compressão automática
  - Criptografia
  - Agendamento via cron
  - Notificações (email, Slack, Discord)`,
}

func Execute() {
	rootCmd.Execute()
}

func init() {
	rootCmd.AddCommand(createCmd)
	rootCmd.AddCommand(listCmd)
	rootCmd.AddCommand(restoreCmd)
	rootCmd.AddCommand(scheduleCmd)
}
// cmd/create.go
package cmd

import (
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/briandowns/spinner"
	"github.com/fatih/color"
	"github.com/spf13/cobra"
)

var (
	source      string
	destination string
	compress    bool
	encrypt     bool
	verbose     bool
)

var createCmd = &cobra.Command{
	Use:   "create",
	Short: "Cria um novo backup",
	Long:  `Cria um backup dos arquivos especificados.`,
	Example: `  # Backup simples
  backup create --source=/home/user/docs --dest=/backup

  # Backup com compressão e criptografia
  backup create --source=/data --dest=/backup --compress --encrypt

  # Backup verbose
  backup create -s /data -d /backup -v`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return runBackup()
	},
}

func init() {
	createCmd.Flags().StringVarP(&source, "source", "s", "", 
		"Diretório fonte (obrigatório)")
	createCmd.Flags().StringVarP(&destination, "dest", "d", "", 
		"Diretório de destino (obrigatório)")
	createCmd.Flags().BoolVarP(&compress, "compress", "c", false, 
		"Comprimir backup")
	createCmd.Flags().BoolVarP(&encrypt, "encrypt", "e", false, 
		"Criptografar backup")
	createCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, 
		"Modo verbose")

	createCmd.MarkFlagRequired("source")
	createCmd.MarkFlagRequired("dest")
}

func runBackup() error {
	// Validar source
	if _, err := os.Stat(source); os.IsNotExist(err) {
		return fmt.Errorf("diretório fonte não existe: %s", source)
	}

	// Criar diretório de destino se não existir
	if err := os.MkdirAll(destination, 0755); err != nil {
		return fmt.Errorf("falha ao criar diretório de destino: %w", err)
	}

	// Gerar nome do backup
	timestamp := time.Now().Format("20060102_150405")
	backupName := fmt.Sprintf("backup_%s", timestamp)
	
	if compress {
		backupName += ".tar.gz"
	} else {
		backupName += ".tar"
	}
	
	if encrypt {
		backupName += ".enc"
	}

	backupPath := filepath.Join(destination, backupName)

	// Spinner para feedback visual
	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
	s.Suffix = " Preparando backup..."
	s.Start()

	// Simular operações (em produção, implementar backup real)
	time.Sleep(500 * time.Millisecond)
	
	if verbose {
		s.Suffix = " Listando arquivos..."
	}
	time.Sleep(500 * time.Millisecond)
	
	if compress {
		s.Suffix = " Comprimindo arquivos..."
		time.Sleep(1 * time.Second)
	}
	
	if encrypt {
		s.Suffix = " Criptografando backup..."
		time.Sleep(1 * time.Second)
	}
	
	s.Suffix = " Finalizando..."
	time.Sleep(300 * time.Millisecond)
	
	s.Stop()

	// Criar arquivo (placeholder)
	f, err := os.Create(backupPath)
	if err != nil {
		return fmt.Errorf("falha ao criar arquivo de backup: %w", err)
	}
	f.Close()

	// Output bonito
	green := color.New(color.FgGreen, color.Bold)
	green.Printf("✓ Backup concluído com sucesso!\n\n")
	
	fmt.Printf("Detalhes:\n")
	fmt.Printf("  Arquivo: %s\n", backupPath)
	fmt.Printf("  Origem:  %s\n", source)
	fmt.Printf("  Tamanho: %s\n", getSize(backupPath))
	
	if compress {
		fmt.Printf("  Compressão: ativada\n")
	}
	if encrypt {
		fmt.Printf("  Criptografia: ativada\n")
	}

	return nil
}

func getSize(path string) string {
	info, err := os.Stat(path)
	if err != nil {
		return "desconhecido"
	}
	
	size := info.Size()
	if size < 1024 {
		return fmt.Sprintf("%d B", size)
	} else if size < 1024*1024 {
		return fmt.Sprintf("%.2f KB", float64(size)/1024)
	} else {
		return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
	}
}

Auto-Complete

Geração de Scripts

// cmd/completion.go
package cmd

import (
	"os"

	"github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
	Use:   "completion [bash|zsh|fish|powershell]",
	Short: "Gera script de auto-complete",
	Long: `Gera scripts de auto-complete para shells.

Para usar:

Bash:
  $ source <(myapp completion bash)
  # Ou permanentemente:
  $ myapp completion bash > /etc/bash_completion.d/myapp

Zsh:
  $ source <(myapp completion zsh)
  # Ou:
  $ myapp completion zsh > "${fpath[1]}/_myapp"

Fish:
  $ myapp completion fish | source
  # Ou:
  $ myapp completion fish > ~/.config/fish/completions/myapp.fish

PowerShell:
  PS> myapp completion powershell | Out-String | Invoke-Expression
  # Ou:
  PS> myapp completion powershell > myapp.ps1`,
	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)
		}
	},
}

Documentação Automática

Gerar Markdown

// tools/gendocs/main.go
package main

import (
	"log"

	"myapp/cmd"
	"github.com/spf13/cobra/doc"
)

func main() {
	err := doc.GenMarkdownTree(cmd.RootCmd, "./docs")
	if err != nil {
		log.Fatal(err)
	}
}

Testes

Testando Comandos

// cmd/serve_test.go
package cmd

import (
	"bytes"
	"testing"

	"github.com/spf13/cobra"
)

func TestServeCommand(t *testing.T) {
	tests := []struct {
		name    string
		args    []string
		wantErr bool
	}{
		{
			name:    "sem flags",
			args:    []string{},
			wantErr: false,
		},
		{
			name:    "com porta",
			args:    []string{"--port=9090"},
			wantErr: false,
		},
		{
			name:    "porta inválida",
			args:    []string{"--port=invalid"},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd := &cobra.Command{}
			cmd.AddCommand(serveCmd)
			
			buf := new(bytes.Buffer)
			cmd.SetOut(buf)
			cmd.SetErr(buf)
			cmd.SetArgs(append([]string{"serve"}, tt.args...))
			
			err := cmd.Execute()
			if (err != nil) != tt.wantErr {
				t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

Distribuição

Cross-Compilation

# Makefile
BINARY_NAME=backup
VERSION=$(shell git describe --tags --always --dirty)
LDFLAGS=-ldflags "-X cmd.Version=$(VERSION) -s -w"

.PHONY: all build clean

all: build

build:
	go build $(LDFLAGS) -o bin/$(BINARY_NAME) ./cmd/backup

# Cross-compilation
build-all:
	# Linux
	GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 ./cmd/backup
	GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-arm64 ./cmd/backup
	
	# macOS
	GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-amd64 ./cmd/backup
	GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-arm64 ./cmd/backup
	
	# Windows
	GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-windows-amd64.exe ./cmd/backup

clean:
	rm -rf bin/

Release com GoReleaser

# .goreleaser.yaml
project_name: backup

builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - amd64
      - arm64

archives:
  - format: tar.gz
    name_template: >-
      {{ .ProjectName }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64
      {{- else if eq .Arch "386" }}i386
      {{- else }}{{ .Arch }}{{ end }}
    format_overrides:
      - goos: windows
        format: zip

checksum:
  name_template: 'checksums.txt'

snapshot:
  name_template: "{{ incpatch .Version }}-next"

changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'

Checklist para CLI Profissional

  • Nome consistente - Comando curto e memorável
  • --help completo - Todos os comandos documentados
  • Validação - Erros claros para input inválido
  • Feedback visual - Progress indicators para operações longas
  • Exit codes - 0 para sucesso, não-zero para erros
  • Config files - Suporte a YAML/JSON/TOML
  • Auto-complete - Scripts para bash/zsh/fish
  • Version flag - --version funcional
  • Logs estruturados - Output consistente
  • Testes - Cobertura de comandos principais

Próximos Passos

Continue sua jornada:

  1. Go e PostgreSQL - Persistência de dados
  2. Go e gRPC - APIs de alta performance
  3. Go Observability - Logs e métricas
  4. Go para Microserviços - Arquitetura distribuída

Crie CLIs poderosas com Go. Compartilhe sua ferramenta!