Herramientas CLI con flag y os

Intermedio
Go
Go
Actualizado: 03/04/2026

¿Por qué Go para herramientas CLI?

Go produce binarios estáticos sin dependencias de runtime. Esto significa que puedes distribuir tu herramienta como un único ejecutable que funciona en cualquier máquina con el mismo sistema operativo y arquitectura, sin instalar nada más. Herramientas como docker, kubectl, terraform y hugo están escritas en Go por esta razón.

Herramientas CLI en Go: argumentos, flags, salida y control

Argumentos crudos: os.Args

os.Args[0] es siempre el nombre del binario. Los argumentos del usuario empiezan en os.Args[1]:

package main

import (
    "fmt"
    "os"
)

func main() {
    programa := os.Args[0]
    args := os.Args[1:]

    if len(args) == 0 {
        fmt.Fprintf(os.Stderr, "Uso: %s <fichero> [fichero...]\n", programa)
        os.Exit(1)
    }

    for _, ruta := range args {
        procesarFichero(ruta)
    }
}

El paquete flag: opciones con tipo y valor por defecto

package main

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

func main() {
    // Definir flags con nombre, valor por defecto y descripción
    host    := flag.String("host", "localhost", "dirección del servidor")
    puerto  := flag.Int("puerto", 8080, "puerto TCP")
    tls     := flag.Bool("tls", false, "activar HTTPS")
    timeout := flag.Duration("timeout", 30*time.Second, "timeout de conexión")

    // Personalizar el mensaje de uso
    flag.Usage = func() {
        fmt.Fprintf(os.Stderr, "Uso: %s [opciones]\n\nOpciones:\n", os.Args[0])
        flag.PrintDefaults()
    }

    flag.Parse() // parsear os.Args[1:]

    fmt.Printf("Conectando a %s:%d (TLS: %v, timeout: %v)\n",
        *host, *puerto, *tls, *timeout)

    // Argumentos posicionales (lo que queda tras los flags)
    restantes := flag.Args()
    fmt.Println("Rutas a procesar:", restantes)
}

Ejecución: ./servidor -host=api.ejemplo.com -puerto=443 -tls fichero1.txt fichero2.txt

Subcomandos con flag.NewFlagSet

El patrón estándar para CLIs con subcomandos (estilo git commit, docker run):

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "Subcomandos disponibles: crear, listar, eliminar")
        os.Exit(1)
    }

    switch os.Args[1] {
    case "crear":
        ejecutarCrear(os.Args[2:])
    case "listar":
        ejecutarListar(os.Args[2:])
    case "eliminar":
        ejecutarEliminar(os.Args[2:])
    default:
        fmt.Fprintf(os.Stderr, "Subcomando desconocido: %q\n", os.Args[1])
        os.Exit(1)
    }
}

func ejecutarCrear(args []string) {
    cmd := flag.NewFlagSet("crear", flag.ExitOnError)
    nombre := cmd.String("nombre", "", "nombre del elemento (obligatorio)")
    tipo   := cmd.String("tipo", "normal", "tipo: normal, urgente, archivado")

    cmd.Parse(args)

    if *nombre == "" {
        fmt.Fprintln(os.Stderr, "Error: -nombre es obligatorio")
        cmd.Usage()
        os.Exit(1)
    }

    fmt.Printf("Creando %q de tipo %q\n", *nombre, *tipo)
}

Variables de entorno

La convención en Go es leer la configuración de entorno en el arranque y fallar explícitamente si falta algo obligatorio:

type Config struct {
    DatabaseURL string
    Puerto      int
    Debug       bool
}

func cargarConfig() (Config, error) {
    dbURL, ok := os.LookupEnv("DATABASE_URL")
    if !ok {
        return Config{}, errors.New("DATABASE_URL no está configurada")
    }

    puertoStr := os.Getenv("PORT")
    if puertoStr == "" {
        puertoStr = "8080"
    }
    puerto, err := strconv.Atoi(puertoStr)
    if err != nil {
        return Config{}, fmt.Errorf("PORT inválido: %w", err)
    }

    return Config{
        DatabaseURL: dbURL,
        Puerto:      puerto,
        Debug:       os.Getenv("DEBUG") == "true",
    }, nil
}

Señales del sistema operativo: apagado limpio

func main() {
    servidor := crearServidor()

    // Canal con buffer 1 para no perder la señal si llegase antes de estar listos
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    // Lanzar servidor en goroutine
    go func() {
        if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Error del servidor: %v", err)
        }
    }()
    log.Println("Servidor iniciado. Ctrl+C para detener.")

    // Bloquear hasta recibir señal
    sig := <-sigs
    log.Printf("Señal recibida: %v. Apagando...", sig)

    // Apagado con timeout de 30 segundos
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := servidor.Shutdown(ctx); err != nil {
        log.Printf("Error en apagado: %v", err)
    }
    log.Println("Servidor apagado correctamente.")
}

Compilación cruzada

# Linux AMD64 (servidores comunes, cloud)
GOOS=linux GOARCH=amd64 go build -o dist/app-linux-amd64 ./cmd/app

# Linux ARM64 (Raspberry Pi, AWS Graviton, Apple Silicon Linux)
GOOS=linux GOARCH=arm64 go build -o dist/app-linux-arm64 ./cmd/app

# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o dist/app-macos-arm64 ./cmd/app

# Windows
GOOS=windows GOARCH=amd64 go build -o dist/app-windows.exe ./cmd/app

# Reducir tamaño del binario (eliminar info de depuración)
go build -ldflags="-s -w" -o dist/app ./cmd/app

El flag -ldflags="-s -w" elimina la tabla de símbolos de depuración y puede reducir el tamaño del binario hasta un 30%.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Go es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Go

Explora más contenido relacionado con Go y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Acceder a argumentos de línea de comandos con os.Args.
  • Definir flags tipados con el paquete flag y flag.NewFlagSet.
  • Implementar subcomandos estilo git/docker con flag.NewFlagSet.
  • Leer y validar variables de entorno con os.Getenv y os.LookupEnv.
  • Gestionar señales del sistema operativo (SIGINT, SIGTERM) para apagado limpio.
  • Compilar binarios estáticos para múltiples plataformas con GOOS y GOARCH.