Automatizando tu vida con Go CLI: Guía profesional para crear herramientas de línea de comandos escalables
Una guía exhaustiva y paso a paso sobre cómo crear herramientas CLI escalables con Go 1.25.5: desde lo básico hasta proyectos empresariales complejos con flags, configuración, logging, y ejemplos prácticos para Windows y Linux.
Cuando alguien menciona Go, muchos piensan en servidores web de alto rendimiento, microservicios escalables, o sistemas distribuidos complejos. Y aunque Go definitivamente excele en esas áreas, hay un aspecto del lenguaje que es igualmente poderoso pero frecuentemente subestimado: su capacidad para crear herramientas de línea de comandos que revolucionan tu flujo de trabajo.
He visto desarrolladores pasar horas escribiendo scripts en Bash que apenas logran ser mantenibles, o Python scripts que requieren gestión de dependencias compleja y tiempos de ejecución lentos. Luego descubren Go y se dan cuenta de que pueden compilar un binario único, portable, sin dependencias, que funciona idénticamente en Windows, macOS y Linux, y que se ejecuta con la velocidad de un programa compilado a nivel nativo.
Pero crear una CLI profesional no es solo escribir código que funcione. Es entender cómo construir herramientas que sean escalables, mantenibles, con experiencia de usuario excepcional, que manejen errores correctamente, que tengan configuración flexible, logging robusto, y que se integren naturalmente en pipelines de automatización complejos.
Este artículo es una guía exhaustiva y paso a paso que te llevará desde tu primer “Hola Mundo” en CLI hasta crear una herramienta de producción compleja que automatiza workflows reales. No es teoría. Es construcción práctica, ejemplo tras ejemplo, explicación tras explicación, decisión tras decisión.
Por Qué Go Es Perfecto para CLI
Antes de empezar a construir, necesitas entender por qué Go es especialmente adecuado para herramientas de línea de comandos.
La Paradoja de Go
Go es un lenguaje que, en la superficie, parece simple. Sin generics (hasta Go 1.18), sin excepciones (usa errores explícitos), sin orientación a objetos en el sentido tradicional. Algunos desarrolladores lo ven como un paso atrás. Pero esta aparente simplicidad es exactamente lo que hace que Go sea perfecto para CLI.
Razón 1: Compilación a Binario Único
Cuando compiles un programa Go, obtienes un único archivo ejecutable. No necesitas Python, no necesitas Ruby, no necesitas una máquina virtual de Java. Simplemente: un binario. Esto significa distribución trivial, sin dependencias de sistema, sin quebraderos de cabeza en ambientes de producción.
# Compila tu herramienta Go
go build -o mi-herramienta
# Funciona en cualquier máquina con el mismo SO/arquitectura
./mi-herramienta --help
Razón 2: Rendimiento Nativo
Go se compila a código máquina. No es interpretado, no es JIT compilado. Es nativo. Una herramienta CLI que procesa 100,000 archivos en Python puede tardar minutos. La misma herramienta en Go: segundos. Esto no es exageración, son diferencias de 10-100x en operaciones intensivas de I/O y procesamiento.
Razón 3: Pensamiento Procedural Natural
Las herramientas de CLI son fundamentalmente procedurales. Haces esto, luego aquello, luego aquello otro. Go fue diseñado exactamente para este tipo de pensamiento. No luchas contra una arquitectura orientada a objetos innecesaria. Escribes lo que necesitas, de la manera más directa posible.
Razón 4: Funcionalidad de Concurrencia Integrada
Cuando necesites procesar 1000 archivos en paralelo, o consultar 100 APIs simultáneamente, Go te da goroutines: concurrencia ligera que es tan simple de usar como go miFunc(). En Bash es pesadilla. En Python requiere aprender async/await o threading complejo. En Go: trivial.
Razón 5: Ecosistema Maduro de Librerías para CLI
Go tiene librerías específicamente diseñadas para crear CLIs profesionales. Manejar flags/argumentos, mostrar progreso, colorear output, crear prompts interactivos, todo está disponible y bien documentado.
Fundamentos: Tu Primer Programa CLI
Setup Inicial
Primero, verifica tu versión de Go:
go version
# Debería mostrar go version go1.25.5 o similar
Si no tienes Go 1.25.5, descargalo desde https://golang.org/dl
Crea tu primer proyecto:
# En una carpeta vacía
mkdir mis-herramientas-cli
cd mis-herramientas-cli
# Inicializa el módulo Go
go mod init github.com/tuusuario/mis-herramientas-cli
Esto crea un go.mod que gestiona tus dependencias:
module github.com/tuusuario/mis-herramientas-cli
go 1.25
El Programa Más Simple
Crea un archivo main.go:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hola, mundo CLI")
}
Ejecuta:
go run main.go
# Output: Hola, mundo CLI
O compila:
go build -o hola-cli
# En Linux/macOS:
./hola-cli
# En Windows:
hola-cli.exe
¿Por qué esto es importante? Ya tienes un binario ejecutable, sin dependencias, que puedes distribuir a cualquiera. Eso es la base.
Manejo de Argumentos y Flags: La Verdadera Potencia CLI
Un programa CLI sin argumentos es como un teléfono sin marcado: no va a ningún lado. Necesitas entender cómo recibir entrada del usuario.
La Biblioteca estándar: flag
Go incluye en su biblioteca estándar el paquete flag, que es suficiente para muchos casos:
package main
import (
"flag"
"fmt"
)
func main() {
// Define tus flags
nombre := flag.String("nombre", "Mundo", "Nombre a saludar")
verbose := flag.Bool("verbose", false, "Mostrar información detallada")
// Parsea los argumentos
flag.Parse()
fmt.Printf("Hola, %s\n", *nombre)
if *verbose {
fmt.Println("Modo verbose activado")
}
}
Uso:
go run main.go
# Output: Hola, Mundo
go run main.go -nombre Omar
# Output: Hola, Omar
go run main.go -nombre Omar -verbose
# Output:
# Hola, Omar
# Modo verbose activado
Puntos importantes:
- Los flags que defines se parsean automáticamente
- El paquete
flaggenera--helpautomáticamente - Devuelven punteros (
*nombre), debes desreferenciarlo
La Alternativa Profesional: pflag (RECOMENDADA)
Para aplicaciones más complejas, usa pflag, que es compatible con GNU-style flags (soporta --flag además de -flag):
go get github.com/spf13/pflag
package main
import (
"fmt"
"github.com/spf13/pflag"
)
func main() {
// Define flags con pflag
nombre := pflag.String("nombre", "Mundo", "Nombre a saludar")
verbose := pflag.BoolP("verbose", "v", false, "Modo verbose")
puerto := pflag.IntP("puerto", "p", 8080, "Puerto a usar")
// Parsea
pflag.Parse()
fmt.Printf("Nombre: %s\n", *nombre)
fmt.Printf("Verbose: %v\n", *verbose)
fmt.Printf("Puerto: %d\n", *puerto)
}
Uso:
go run main.go --nombre Omar -v --puerto 3000
# Output:
# Nombre: Omar
# Verbose: true
# Puerto: 3000
Ventajas sobre flag:
- Soporta forma larga (
--nombre) y corta (-n) - Más legible para usuarios
- Compatible con herramientas Unix estándar
Cobra: El Framework para CLIs Profesionales
Cuando tu aplicación crece y necesitas subcomandos (como git commit, docker run, kubectl apply), flag y pflag se quedan cortos. Aquí entra Cobra, el framework estándar de la industria para CLIs en Go.
Instalación y Setup
# Instala Cobra y su generador de código
go get -u github.com/spf13/cobra/cmd/cobra
go install github.com/spf13/cobra/cmd/cobra@latest
# Verifica la instalación
cobra --version
Generando tu Primera Aplicación Cobra
# Crea una aplicación nueva
cobra-cli init tareas
cd tareas
Esto crea una estructura profesional:
tareas/
├── main.go
├── cmd/
│ └── root.go
├── go.mod
├── go.sum
└── LICENSE
main.go:
package main
import "tareas/cmd"
func main() {
cmd.Execute()
}
cmd/root.go:
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd representa el comando base
var rootCmd = &cobra.Command{
Use: "tareas",
Short: "Gestor de tareas desde CLI",
Long: `tareas es una herramienta CLI para gestionar tus tareas diarias
desde la línea de comandos, perfecta para automatizar flujos de trabajo.`,
Run: func(cmd *cobra.Command, args []string) {
// Acción del comando principal
cmd.Help()
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Aquí defines flags globales que aplican a todos los comandos
}
Compila y prueba:
go run main.go --help
# Automáticamente genera ayuda profesional
Agregando Subcomandos
cobra-cli add crear
cobra-cli add listar
cobra-cli add eliminar
Esto crea cmd/crear.go, cmd/listar.go, cmd/eliminar.go. Edita cmd/crear.go:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var crearCmd = &cobra.Command{
Use: "crear [nombre]",
Short: "Crea una nueva tarea",
Long: `Crea una nueva tarea en tu lista personal.
Ejemplo: tareas crear "Hacer la compra"`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
titulo := args[0]
fmt.Printf("✓ Tarea creada: %s\n", titulo)
},
}
func init() {
rootCmd.AddCommand(crearCmd)
// Flags específicos del comando crear
crearCmd.Flags().StringP("prioridad", "p", "normal", "Prioridad: baja, normal, alta")
crearCmd.Flags().StringP("fecha", "f", "", "Fecha límite (YYYY-MM-DD)")
}
Uso:
go run main.go crear "Escribir artículo" -p alta -f 2025-12-31
# Output: ✓ Tarea creada: Escribir artículo
Manejo de Archivos y Configuración
Cualquier herramienta CLI profesional necesita almacenar estado. Las tareas que creaste deben persistir. Aquí vamos a crear un sistema de almacenamiento basado en archivos JSON.
Estructura de Datos
Crea pkg/tarea/tarea.go:
package tarea
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// Tarea representa una tarea individual
type Tarea struct {
ID string `json:"id"`
Titulo string `json:"titulo"`
Descripcion string `json:"descripcion,omitempty"`
Prioridad string `json:"prioridad"` // baja, normal, alta
Estado string `json:"estado"` // pendiente, completada
Creada time.Time `json:"creada"`
Vencimiento time.Time `json:"vencimiento,omitempty"`
Completada *time.Time `json:"completada,omitempty"`
}
// Store gestiona el almacenamiento de tareas
type Store struct {
archivoPath string
}
// NewStore crea un nuevo almacén
func NewStore() (*Store, error) {
// Determina dónde almacenar los datos
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("no se pudo obtener el directorio home: %w", err)
}
// Crea .tareas-cli/datos en el home del usuario
dataDir := filepath.Join(home, ".tareas-cli")
if err := os.MkdirAll(dataDir, 0755); err != nil {
return nil, fmt.Errorf("error creando directorio de datos: %w", err)
}
archivoPath := filepath.Join(dataDir, "tareas.json")
return &Store{
archivoPath: archivoPath,
}, nil
}
// Cargar lee todas las tareas del archivo
func (s *Store) Cargar() ([]Tarea, error) {
// Si el archivo no existe, retorna lista vacía
if _, err := os.Stat(s.archivoPath); os.IsNotExist(err) {
return []Tarea{}, nil
}
datos, err := os.ReadFile(s.archivoPath)
if err != nil {
return nil, fmt.Errorf("error leyendo archivo: %w", err)
}
var tareas []Tarea
if err := json.Unmarshal(datos, &tareas); err != nil {
return nil, fmt.Errorf("error parseando JSON: %w", err)
}
return tareas, nil
}
// Guardar persiste todas las tareas al archivo
func (s *Store) Guardar(tareas []Tarea) error {
datos, err := json.MarshalIndent(tareas, "", " ")
if err != nil {
return fmt.Errorf("error serializando JSON: %w", err)
}
if err := os.WriteFile(s.archivoPath, datos, 0644); err != nil {
return fmt.Errorf("error escribiendo archivo: %w", err)
}
return nil
}
// Agregar añade una nueva tarea
func (s *Store) Agregar(titulo, descripcion, prioridad, fechaVencimiento string) (string, error) {
tareas, err := s.Cargar()
if err != nil {
return "", err
}
// Genera un ID único (simple)
id := fmt.Sprintf("tarea_%d", time.Now().UnixNano())
tarea := Tarea{
ID: id,
Titulo: titulo,
Descripcion: descripcion,
Prioridad: prioridad,
Estado: "pendiente",
Creada: time.Now(),
}
// Parsea fecha de vencimiento si se proporciona
if fechaVencimiento != "" {
venc, err := time.Parse("2006-01-02", fechaVencimiento)
if err != nil {
return "", fmt.Errorf("formato de fecha inválido, usa YYYY-MM-DD: %w", err)
}
tarea.Vencimiento = venc
}
tareas = append(tareas, tarea)
if err := s.Guardar(tareas); err != nil {
return "", err
}
return id, nil
}
// Listar retorna todas las tareas con filtros opcionales
func (s *Store) Listar(estado string) ([]Tarea, error) {
tareas, err := s.Cargar()
if err != nil {
return nil, err
}
if estado == "" {
return tareas, nil
}
var filtradas []Tarea
for _, t := range tareas {
if t.Estado == estado {
filtradas = append(filtradas, t)
}
}
return filtradas, nil
}
// Completar marca una tarea como completada
func (s *Store) Completar(id string) error {
tareas, err := s.Cargar()
if err != nil {
return err
}
for i, t := range tareas {
if t.ID == id {
ahora := time.Now()
tareas[i].Completada = &ahora
tareas[i].Estado = "completada"
return s.Guardar(tareas)
}
}
return fmt.Errorf("tarea no encontrada: %s", id)
}
// Eliminar remueve una tarea
func (s *Store) Eliminar(id string) error {
tareas, err := s.Cargar()
if err != nil {
return err
}
var nuevas []Tarea
encontrada := false
for _, t := range tareas {
if t.ID != id {
nuevas = append(nuevas, t)
} else {
encontrada = true
}
}
if !encontrada {
return fmt.Errorf("tarea no encontrada: %s", id)
}
return s.Guardar(nuevas)
}
Integración con Cobra
Actualiza cmd/crear.go:
package cmd
import (
"fmt"
"tareas/pkg/tarea"
"github.com/spf13/cobra"
)
var crearCmd = &cobra.Command{
Use: "crear [titulo]",
Short: "Crea una nueva tarea",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Obtén los flags
prioridad, _ := cmd.Flags().GetString("prioridad")
fecha, _ := cmd.Flags().GetString("fecha")
descripcion, _ := cmd.Flags().GetString("descripcion")
// Crea el almacén
store, err := tarea.NewStore()
if err != nil {
return fmt.Errorf("error inicializando almacén: %w", err)
}
// Agrega la tarea
id, err := store.Agregar(args[0], descripcion, prioridad, fecha)
if err != nil {
return err
}
fmt.Printf("✓ Tarea creada con ID: %s\n", id)
return nil
},
}
func init() {
rootCmd.AddCommand(crearCmd)
crearCmd.Flags().StringP("prioridad", "p", "normal", "Prioridad: baja, normal, alta")
crearCmd.Flags().StringP("fecha", "f", "", "Fecha límite (YYYY-MM-DD)")
crearCmd.Flags().StringP("descripcion", "d", "", "Descripción detallada")
}
Crea cmd/listar.go:
package cmd
import (
"fmt"
"tareas/pkg/tarea"
"github.com/spf13/cobra"
)
var listarCmd = &cobra.Command{
Use: "listar",
Short: "Lista todas tus tareas",
RunE: func(cmd *cobra.Command, args []string) error {
estado, _ := cmd.Flags().GetString("estado")
store, err := tarea.NewStore()
if err != nil {
return err
}
tareas, err := store.Listar(estado)
if err != nil {
return err
}
if len(tareas) == 0 {
fmt.Println("No hay tareas que mostrar")
return nil
}
// Imprime en formato tabla
fmt.Printf("\n%-8s | %-30s | %-8s | %-10s\n", "ID", "Título", "Prioridad", "Estado")
fmt.Println(string(make([]byte, 70)))
for _, t := range tareas {
// Limita el título a 30 caracteres
titulo := t.Titulo
if len(titulo) > 30 {
titulo = titulo[:27] + "..."
}
fmt.Printf("%-8s | %-30s | %-8s | %-10s\n",
t.ID, titulo, t.Prioridad, t.Estado)
}
fmt.Println()
return nil
},
}
func init() {
rootCmd.AddCommand(listarCmd)
listarCmd.Flags().StringP("estado", "e", "", "Filtrar por estado (pendiente, completada)")
}
Crea cmd/completar.go:
package cmd
import (
"fmt"
"tareas/pkg/tarea"
"github.com/spf13/cobra"
)
var completarCmd = &cobra.Command{
Use: "completar [id]",
Short: "Marca una tarea como completada",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
store, err := tarea.NewStore()
if err != nil {
return err
}
if err := store.Completar(args[0]); err != nil {
return err
}
fmt.Printf("✓ Tarea %s marcada como completada\n", args[0])
return nil
},
}
func init() {
rootCmd.AddCommand(completarCmd)
}
Prueba tu aplicación:
go run main.go crear "Aprender Go"
go run main.go crear "Escribir código" -p alta
go run main.go listar
go run main.go completar tarea_1734800000000000000
Colorización y Salida Formateada: Experiencia de Usuario
Las herramientas CLI modernas no son aburridas. Agreguemos colores y formateo profesional con termui o lipgloss de Charm Inc.
Instalación
go get github.com/charmbracelet/lipgloss
Crear un Paquete de Formatos
Crea pkg/ui/ui.go:
package ui
import (
"fmt"
"github.com/charmbracelet/lipgloss"
)
// Colores y estilos
var (
SuccessStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("10")). // Verde
Bold(true)
ErrorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("9")). // Rojo
Bold(true)
WarningStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("11")). // Amarillo
Bold(true)
HeaderStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("12")). // Azul
Bold(true).
Underline(true)
PriorityAlta = lipgloss.NewStyle().
Foreground(lipgloss.Color("9"))
PriorityMedia = lipgloss.NewStyle().
Foreground(lipgloss.Color("11"))
PriorityBaja = lipgloss.NewStyle().
Foreground(lipgloss.Color("10"))
)
// Success imprime un mensaje de éxito
func Success(msg string) {
fmt.Println(SuccessStyle.Render("✓ " + msg))
}
// Error imprime un mensaje de error
func Error(msg string) {
fmt.Println(ErrorStyle.Render("✗ " + msg))
}
// Warning imprime un mensaje de advertencia
func Warning(msg string) {
fmt.Println(WarningStyle.Render("⚠ " + msg))
}
// Header imprime un encabezado
func Header(msg string) {
fmt.Println(HeaderStyle.Render(msg))
fmt.Println()
}
// PriorityColor retorna un color basado en prioridad
func PriorityColor(prioridad string) string {
switch prioridad {
case "alta":
return PriorityAlta.Render(prioridad)
case "media", "normal":
return PriorityMedia.Render(prioridad)
case "baja":
return PriorityBaja.Render(prioridad)
default:
return prioridad
}
}
Actualiza cmd/listar.go para usar estos estilos:
package cmd
import (
"fmt"
"tareas/pkg/tarea"
"tareas/pkg/ui"
"github.com/spf13/cobra"
)
var listarCmd = &cobra.Command{
Use: "listar",
Short: "Lista todas tus tareas",
RunE: func(cmd *cobra.Command, args []string) error {
estado, _ := cmd.Flags().GetString("estado")
store, err := tarea.NewStore()
if err != nil {
return err
}
tareas, err := store.Listar(estado)
if err != nil {
return err
}
if len(tareas) == 0 {
ui.Warning("No hay tareas que mostrar")
return nil
}
ui.Header("📋 Tus Tareas")
// Imprime con colores
for i, t := range tareas {
titulo := t.Titulo
if len(titulo) > 40 {
titulo = titulo[:37] + "..."
}
// Símbolo de estado
estadoSimbolo := "◯"
if t.Estado == "completada" {
estadoSimbolo = "✓"
}
fmt.Printf("%d. %s %s [%s] %s\n",
i+1,
estadoSimbolo,
titulo,
ui.PriorityColor(t.Prioridad),
t.ID)
}
fmt.Println()
return nil
},
}
func init() {
rootCmd.AddCommand(listarCmd)
listarCmd.Flags().StringP("estado", "e", "", "Filtrar por estado (pendiente, completada)")
}
Flags Avanzados y Configuración
Para herramientas profesionales, necesitas configuración que persista. Vamos a agregar un archivo de configuración global.
Sistema de Configuración
Crea pkg/config/config.go:
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// Config representa la configuración global
type Config struct {
Editor string `json:"editor,omitempty"`
DateFormat string `json:"date_format"`
Theme string `json:"theme"`
AutoSort bool `json:"auto_sort"`
DefaultPriority string `json:"default_priority"`
}
// DefaultConfig retorna la configuración por defecto
func DefaultConfig() Config {
return Config{
DateFormat: "2006-01-02",
Theme: "default",
AutoSort: true,
DefaultPriority: "normal",
}
}
// GetConfigPath retorna la ruta del archivo de configuración
func GetConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := filepath.Join(home, ".tareas-cli")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", err
}
return filepath.Join(configDir, "config.json"), nil
}
// Load carga la configuración del archivo
func Load() (Config, error) {
configPath, err := GetConfigPath()
if err != nil {
return DefaultConfig(), err
}
// Si el archivo no existe, usa defaults
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return DefaultConfig(), nil
}
datos, err := os.ReadFile(configPath)
if err != nil {
return DefaultConfig(), err
}
var cfg Config
if err := json.Unmarshal(datos, &cfg); err != nil {
return DefaultConfig(), err
}
return cfg, nil
}
// Save persiste la configuración
func Save(cfg Config) error {
configPath, err := GetConfigPath()
if err != nil {
return err
}
datos, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(configPath, datos, 0644)
}
Comando para Configuración
Crea cmd/config.go:
package cmd
import (
"fmt"
"tareas/pkg/config"
"tareas/pkg/ui"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Gestiona la configuración",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.Load()
if err != nil {
return err
}
ui.Header("⚙️ Configuración Actual")
fmt.Printf("Tema: %s\n", cfg.Theme)
fmt.Printf("Formato de fecha: %s\n", cfg.DateFormat)
fmt.Printf("Ordenar automáticamente: %v\n", cfg.AutoSort)
fmt.Printf("Prioridad por defecto: %s\n", cfg.DefaultPriority)
return nil
},
}
var configSetCmd = &cobra.Command{
Use: "set [clave] [valor]",
Short: "Establece una opción de configuración",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clave := args[0]
valor := args[1]
cfg, err := config.Load()
if err != nil {
return err
}
switch clave {
case "theme":
cfg.Theme = valor
case "date-format":
cfg.DateFormat = valor
case "default-priority":
if valor != "baja" && valor != "normal" && valor != "alta" {
ui.Error("Prioridad inválida. Usa: baja, normal, alta")
return nil
}
cfg.DefaultPriority = valor
default:
ui.Error(fmt.Sprintf("Opción desconocida: %s", clave))
return nil
}
if err := config.Save(cfg); err != nil {
return err
}
ui.Success(fmt.Sprintf("Configuración actualizada: %s = %s", clave, valor))
return nil
},
}
func init() {
rootCmd.AddCommand(configCmd)
configCmd.AddCommand(configSetCmd)
}
Logging Profesional
Las herramientas de producción necesitan logging. Vamos a usar zap, que es el estándar de Go para logging estructurado.
Instalación
go get go.uber.org/zap
Setup de Logging
Crea pkg/logger/logger.go:
package logger
import (
"fmt"
"os"
"path/filepath"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.Logger
// Init inicializa el logger
func Init(verbose bool) error {
// Determina nivel de log
level := zapcore.InfoLevel
if verbose {
level = zapcore.DebugLevel
}
// Configuración del logger
config := zap.NewProductionConfig()
config.Level = zap.NewAtomicLevelAt(level)
// Si verbose, imprime en consola; si no, escribe en archivo
if verbose {
config.OutputPaths = []string{"stdout"}
} else {
// Escribe logs en ~/.tareas-cli/logs
home, err := os.UserHomeDir()
if err != nil {
return err
}
logsDir := filepath.Join(home, ".tareas-cli", "logs")
if err := os.MkdirAll(logsDir, 0755); err != nil {
return err
}
logFile := filepath.Join(logsDir, "app.log")
config.OutputPaths = []string{logFile}
}
logger, err := config.Build()
if err != nil {
return fmt.Errorf("error inicializando logger: %w", err)
}
Logger = logger
return nil
}
// Close cierra el logger correctamente
func Close() error {
if Logger != nil {
return Logger.Sync()
}
return nil
}
Actualiza cmd/root.go para inicializar logging:
package cmd
import (
"os"
"tareas/pkg/logger"
"github.com/spf13/cobra"
)
var verbose bool
var rootCmd = &cobra.Command{
Use: "tareas",
Short: "Gestor de tareas desde CLI",
Long: `tareas es una herramienta CLI para gestionar tus tareas`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
logger.Init(verbose)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
logger.Close()
},
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Modo verbose")
}
Proyecto Complejo: Automatizador de Tareas del Sistema
Ahora vamos a crear algo más ambicioso: una herramienta que automatiza tareas del sistema. Funciona en Windows y Linux.
Estructura
automatizador/
├── main.go
├── cmd/
│ ├── root.go
│ ├── backup.go
│ ├── clean.go
│ └── schedule.go
├── pkg/
│ ├── backup/
│ ├── cleaner/
│ └── scheduler/
└── go.mod
Módulo de Backup
Crea pkg/backup/backup.go:
package backup
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
// BackupConfig configura un backup
type BackupConfig struct {
SourcePath string // Carpeta a respaldar
DestPath string // Dónde guardar el backup
Exclude []string // Patrones a excluir (ej: .git, node_modules)
}
// Backup crea un archivo comprimido del directorio especificado
func Backup(cfg BackupConfig) (string, error) {
// Valida que la carpeta source existe
sourceInfo, err := os.Stat(cfg.SourcePath)
if err != nil {
return "", fmt.Errorf("error accediendo a source: %w", err)
}
if !sourceInfo.IsDir() {
return "", fmt.Errorf("source no es un directorio: %s", cfg.SourcePath)
}
// Crea el nombre del archivo backup
timestamp := time.Now().Format("2006-01-02_150405")
baseName := filepath.Base(cfg.SourcePath)
backupName := fmt.Sprintf("%s_%s.zip", baseName, timestamp)
backupPath := filepath.Join(cfg.DestPath, backupName)
// Crea el directorio destino si no existe
if err := os.MkdirAll(cfg.DestPath, 0755); err != nil {
return "", fmt.Errorf("error creando directorio destino: %w", err)
}
// Abre el archivo zip para escribir
zipFile, err := os.Create(backupPath)
if err != nil {
return "", fmt.Errorf("error creando archivo zip: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// Camina por todos los archivos
err = filepath.Walk(cfg.SourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Obtén la ruta relativa
relPath, err := filepath.Rel(cfg.SourcePath, path)
if err != nil {
return err
}
// Chequea si está en la lista de exclusión
if shouldExclude(relPath, cfg.Exclude) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Si es un directorio, solo crea la entrada
if info.IsDir() {
return nil
}
// Abre el archivo para lectura
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// Crea una entrada en el zip
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = relPath
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
// Copia el contenido del archivo
if _, err := io.Copy(writer, file); err != nil {
return err
}
return nil
})
if err != nil {
os.Remove(backupPath)
return "", fmt.Errorf("error durante backup: %w", err)
}
return backupPath, nil
}
// shouldExclude chequea si un path debe excluirse
func shouldExclude(path string, patterns []string) bool {
for _, pattern := range patterns {
if matched, _ := filepath.Match(pattern, filepath.Base(path)); matched {
return true
}
if matched, _ := filepath.Match(pattern, path); matched {
return true
}
}
return false
}
// GetSize retorna el tamaño del archivo backup en bytes
func GetSize(path string) (int64, error) {
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}
Comando de Backup
Crea cmd/backup.go:
package cmd
import (
"fmt"
"os"
"path/filepath"
"tareas/pkg/backup"
"tareas/pkg/ui"
"github.com/spf13/cobra"
)
var backupCmd = &cobra.Command{
Use: "backup [source] [dest]",
Short: "Crea un backup comprimido de una carpeta",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
source := args[0]
dest := args[1]
// Convierte a rutas absolutas
sourcePath, err := filepath.Abs(source)
if err != nil {
return fmt.Errorf("error resolviendo path source: %w", err)
}
destPath, err := filepath.Abs(dest)
if err != nil {
return fmt.Errorf("error resolviendo path destino: %w", err)
}
// Valida que source existe
if _, err := os.Stat(sourcePath); os.IsNotExist(err) {
ui.Error(fmt.Sprintf("La carpeta no existe: %s", sourcePath))
return nil
}
ui.Header("🔄 Iniciando backup...")
// Configura el backup
cfg := backup.BackupConfig{
SourcePath: sourcePath,
DestPath: destPath,
Exclude: []string{
".git",
"node_modules",
".DS_Store",
"*.tmp",
},
}
// Ejecuta el backup
backupPath, err := backup.Backup(cfg)
if err != nil {
ui.Error(fmt.Sprintf("Error durante backup: %v", err))
return nil
}
// Obtén el tamaño
size, _ := backup.GetSize(backupPath)
sizeMB := float64(size) / 1024 / 1024
ui.Success(fmt.Sprintf("Backup completado: %s (%.2f MB)",
filepath.Base(backupPath), sizeMB))
return nil
},
}
func init() {
rootCmd.AddCommand(backupCmd)
backupCmd.Flags().StringSliceP("exclude", "e", []string{}, "Patrones a excluir")
}
Interactividad: Prompts y Confirmaciones
Usa promptui de Manifold para crear experiencias interactivas:
go get github.com/manifoldco/promptui
Módulo de Interacción
Crea pkg/interactive/interactive.go:
package interactive
import (
"fmt"
"github.com/manifoldco/promptui"
)
// Confirm pregunta al usuario por confirmación
func Confirm(prompt string) (bool, error) {
p := promptui.Select{
Label: prompt,
Items: []string{"Sí", "No"},
}
_, result, err := p.Run()
if err != nil {
return false, err
}
return result == "Sí", nil
}
// Input solicita entrada de texto
func Input(label string) (string, error) {
p := promptui.Prompt{
Label: label,
}
return p.Run()
}
// Select permite seleccionar de una lista
func Select(label string, items []string) (string, error) {
p := promptui.Select{
Label: label,
Items: items,
}
_, result, err := p.Run()
return result, err
}
// MultiSelect permite seleccionar múltiples items
func MultiSelect(label string, items []string) ([]string, error) {
p := promptui.Select{
Label: label,
Items: items,
Size: len(items),
}
var selected []string
for {
_, choice, err := p.Run()
if err != nil {
break
}
selected = append(selected, choice)
}
if len(selected) == 0 {
return nil, fmt.Errorf("no seleccionaste nada")
}
return selected, nil
}
Uso en Comandos
Actualiza cmd/crear.go para usar prompts si no se proporciona título:
package cmd
import (
"fmt"
"tareas/pkg/interactive"
"tareas/pkg/tarea"
"tareas/pkg/ui"
"github.com/spf13/cobra"
)
var crearCmd = &cobra.Command{
Use: "crear [titulo]",
Short: "Crea una nueva tarea",
RunE: func(cmd *cobra.Command, args []string) error {
var titulo string
// Si no se proporciona título, pide interactivamente
if len(args) == 0 {
var err error
titulo, err = interactive.Input("Título de la tarea")
if err != nil {
return fmt.Errorf("cancelado")
}
} else {
titulo = args[0]
}
// Obtén prioridad interactivamente
prioridad, _ := cmd.Flags().GetString("prioridad")
if prioridad == "normal" {
p, err := interactive.Select("Prioridad", []string{"baja", "normal", "alta"})
if err == nil {
prioridad = p
}
}
// Resto del código...
store, err := tarea.NewStore()
if err != nil {
return err
}
id, err := store.Agregar(titulo, "", prioridad, "")
if err != nil {
return err
}
ui.Success(fmt.Sprintf("Tarea creada con ID: %s", id))
return nil
},
}
func init() {
rootCmd.AddCommand(crearCmd)
crearCmd.Flags().StringP("prioridad", "p", "normal", "Prioridad")
}
Procesamiento de Archivos en Masa: Concurrencia
Uno de los mayores poderes de Go es la concurrencia. Vamos a procesar miles de archivos en paralelo.
Procesador de Archivos
Crea pkg/fileprocessor/processor.go:
package fileprocessor
import (
"fmt"
"os"
"path/filepath"
"sync"
)
// FileProcessor procesa archivos de manera concurrente
type FileProcessor struct {
Workers int // Número de goroutines concurrentes
callback func(path string) error
}
// New crea un nuevo procesador
func New(workers int, callback func(path string) error) *FileProcessor {
return &FileProcessor{
Workers: workers,
callback: callback,
}
}
// ProcessDirectory procesa todos los archivos en un directorio
func (fp *FileProcessor) ProcessDirectory(dirPath string, pattern string) error {
// Recolecta todos los archivos
var files []string
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Si hay patrón, filtra
if pattern == "" || matchPattern(filepath.Base(path), pattern) {
files = append(files, path)
}
}
return nil
})
if err != nil {
return err
}
// Procesa los archivos con concurrencia
return fp.processFiles(files)
}
// processFiles procesa una lista de archivos concurrentemente
func (fp *FileProcessor) processFiles(files []string) error {
// Crea canales
jobsChan := make(chan string, fp.Workers)
errorsChan := make(chan error, len(files))
var wg sync.WaitGroup
// Inicia workers
for i := 0; i < fp.Workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for filePath := range jobsChan {
if err := fp.callback(filePath); err != nil {
errorsChan <- fmt.Errorf("error procesando %s: %w", filePath, err)
}
}
}()
}
// Envía trabajos
for _, file := range files {
jobsChan <- file
}
close(jobsChan)
// Espera a que terminen
wg.Wait()
close(errorsChan)
// Recolecta errores
var errs []error
for err := range errorsChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return fmt.Errorf("se encontraron %d errores durante procesamiento", len(errs))
}
return nil
}
// matchPattern hace match simple de patrón
func matchPattern(name, pattern string) bool {
matched, _ := filepath.Match(pattern, name)
return matched
}
Comando para Limpiar Archivos Temporales
Crea cmd/limpiar.go:
package cmd
import (
"fmt"
"os"
"path/filepath"
"tareas/pkg/fileprocessor"
"tareas/pkg/ui"
"github.com/spf13/cobra"
)
var limpiarCmd = &cobra.Command{
Use: "limpiar [directorio]",
Short: "Limpia archivos temporales",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
dirPath := args[0]
// Valida que existe
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
ui.Error(fmt.Sprintf("Directorio no existe: %s", dirPath))
return nil
}
ui.Header("🧹 Limpiando archivos temporales...")
// Archivos a buscar
patterns := []string{"*.tmp", "*.log", "*.cache", ".DS_Store"}
deletedCount := 0
deletedSize := int64(0)
for _, pattern := range patterns {
processor := fileprocessor.New(4, func(filePath string) error {
info, err := os.Stat(filePath)
if err != nil {
return err
}
if err := os.Remove(filePath); err != nil {
return err
}
deletedCount++
deletedSize += info.Size()
fmt.Printf(" Eliminado: %s\n", filepath.Base(filePath))
return nil
})
processor.ProcessDirectory(dirPath, pattern)
}
sizeMB := float64(deletedSize) / 1024 / 1024
ui.Success(fmt.Sprintf("Limpieza completada: %d archivos, %.2f MB liberados",
deletedCount, sizeMB))
return nil
},
}
func init() {
rootCmd.AddCommand(limpiarCmd)
}
Distribución y Construcción Multiplataforma
Go hace trivial compilar para múltiples plataformas:
Build Script
Crea un archivo build.sh:
#!/bin/bash
# Build para diferentes plataformas
APP_NAME="tareas"
VERSION="1.0.0"
# Linux
GOOS=linux GOARCH=amd64 go build -o dist/${APP_NAME}-linux-amd64 -ldflags="-X main.Version=${VERSION}"
# macOS
GOOS=darwin GOARCH=amd64 go build -o dist/${APP_NAME}-darwin-amd64 -ldflags="-X main.Version=${VERSION}"
GOOS=darwin GOARCH=arm64 go build -o dist/${APP_NAME}-darwin-arm64 -ldflags="-X main.Version=${VERSION}"
# Windows
GOOS=windows GOARCH=amd64 go build -o dist/${APP_NAME}-windows-amd64.exe -ldflags="-X main.Version=${VERSION}"
echo "✓ Compilación completada"
ls -lh dist/
Ejecuta:
chmod +x build.sh
./build.sh
Escalabilidad: De Herramienta a Plataforma
Cuando tu CLI crece, considera:
1. Plugin System
Go permite cargar plugins dinámicamente:
// Carga plugins de extensión
plugin, err := plugin.Open("./plugins/myplugin.so")
if err != nil {
return err
}
sym, err := plugin.Lookup("Execute")
if err != nil {
return err
}
execute := sym.(func() error)
execute()
2. Actualización Automática
go get -u github.com/inconshreveable/go-update
Permite que tu CLI se actualice a sí misma.
3. Documentación Generada
Cobra puede generar documentación automáticamente:
import "github.com/spf13/cobra/doc"
doc.GenMarkdownTree(rootCmd, "./docs")
Recomendaciones Finales para Producción
1. Error Handling Robusto
Siempre usa %w en fmt.Errorf para preservar la cadena de errores:
// ❌ Malo
if err != nil {
return fmt.Errorf("error: %v", err)
}
// ✅ Bueno
if err != nil {
return fmt.Errorf("procesando archivo: %w", err)
}
2. Testing
// main_test.go
package main
import (
"testing"
)
func TestExecution(t *testing.T) {
// Pruebas
}
Ejecuta con go test ./...
3. Versionado Semántico
Incluye versión en tu binario:
var Version = "1.0.0"
var versionCmd = &cobra.Command{
Use: "version",
Short: "Muestra la versión",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s\n", Version)
},
}
Compila con:
go build -ldflags="-X main.Version=1.0.0"
4. Documentación
// Asegúrate que cada paquete tiene documentación
// Package tarea proporciona funcionalidad para gestionar tareas.
package tarea
// Tarea representa una tarea individual en el sistema.
type Tarea struct {
// ID es el identificador único
ID string
}
Genera docs con go doc ./...
Conclusión
Go es una herramienta extraordinaria para crear CLIs que funcionen en cualquier lugar, que se ejecuten rápidamente, y que sean fáciles de mantener. Has aprendido desde lo básico hasta crear herramientas complejas y escalables.
Los puntos clave:
- Cobra para estructura profesional
- pflag para argumentos avanzados
- Concurrencia para operaciones en masa
- JSON para persistencia simple
- lipgloss para UX
- zap para logging profesional
El siguiente paso es crear tu propia herramienta. Automatiza algo que haces repetidamente, empaqu etala como binario, y distribúyela. Eso es el verdadero poder de Go.
Tags
Artículos relacionados
API Versioning Strategies: Cómo Evolucionar APIs sin Romper Clientes
Una guía exhaustiva sobre estrategias de versionado de APIs: URL versioning vs Header versioning, cómo deprecar endpoints sin shock, migration patterns reales, handling de cambios backwards-incompatibles, y decisiones arquitectónicas que importan. Con 50+ ejemplos de código en Go.
La Capa de Repositorio en Go: Conexiones, Queries y Arquitectura Agnóstica
Una guía exhaustiva sobre cómo construir la capa de repositorio en Go: arquitectura hexagonal con ports, conexiones a MongoDB, PostgreSQL, SQLite, manejo de queries, validación, escalabilidad y cómo cambiar de base de datos sin tocar la lógica de negocio.
Event-Driven Architecture: Más Allá de Pub/Sub - Choreography, Orchestration y Garantías de Entrega
Una guía profunda sobre arquitectura orientada a eventos en Go: diferencia entre choreography y orchestration, cómo manejar dead letter queues, retries inteligentes, garantías de entrega en REST, y patrones verificables en producción.