Lecciones del módulo (4/5)
Mini-proyecto: una CLI con banderas
Juntemos los componentes básicos de los módulos anteriores para crear una pequeña CLI idiomática: análisis de indicadores con el paquete flag, manejo de argumentos posicionales, errores en os.Stderr y el código de salida correcto.
El paquete flag
package main
import (
"flag"
"fmt"
"os"
)
func main() {
n := flag.Int("n", 1, "number of repetitions")
upper := flag.Bool("u", false, "print in uppercase")
flag.Parse()
args := flag.Args() // remaining positional arguments
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
os.Exit(2)
}
text := args[0]
if *upper {
text = strings.ToUpper(text)
}
for i := 0; i < *n; i++ {
fmt.Println(text)
}
}Puntos clave:
flag.Int,flag.String,flag.Bool,flag.Duration(etc.) devuelven un puntero: usas*n,*upper.flag.Parse()debe llamarse UNA VEZ, antes de leer los valores y antes deflag.Args().flag.Args()devuelve los argumentos posicionales (aquellos después de--o después del último indicador).- Formularios aceptados:
flag.String0,flag.String1,flag.String2. Booleanos:flag.String3 (verdadero),flag.String4.
Códigos de salida convencionales
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)Los shells y los scripts esperan estos códigos: utilícelos de forma coherente. Nunca salga de una rutina no principal con os.Exit: omite cada defer, incluido el vaciado de log.
Stderr frente a Stdout
Antigua y sagrada regla de Unix:
os.Stdout→ salida útil, canalizable (cli | grep ...).os.Stderr→ diagnóstico, errores, barras de progreso, uso.
fmt.Println("result: 42") // stdout
fmt.Fprintln(os.Stderr, "error: ...") // stderrDe esta manera cli 2>/dev/null muestra solo los resultados, cli >out.txt redirige solo el resultado y los errores permanecen visibles.
CÓDIGOPH0 frente a CÓDIGOPH1
fmtpara salida del usuario.logpara diagnóstico: agrega una marca de tiempo, escribe enos.Stderrde forma predeterminada, tienelog.Fatal(registros +os.Exit(1)) ylog.Panic.
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1Para aplicaciones modernas (Go 1.21+), use log/slog para el registro estructurado (JSON o texto, niveles, campos escritos).
Arquitectura: separar main de la lógica
func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(args []string, stdout, stderr io.Writer) error {
// here use a new flag.FlagSet instead of the global flag.CommandLine
fs := flag.NewFlagSet("echo", flag.ContinueOnError)
fs.SetOutput(stderr)
n := fs.Int("n", 1, "repetitions")
if err := fs.Parse(args); err != nil {
return err
}
// ...
return nil
}Ventajas:
- Comprobable: pasa
bytes.Buffercomo stdout/stderr y verifica la salida. - Sin estado global: cada
FlagSetestá aislado. mainse queda pequeño: prepara IO + delegados.
Subcomandos
Para CLI con subcomandos (mycli serve, mycli migrate), un esquema sin dependencias externas:
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "serve":
serveCmd(os.Args[2:])
case "migrate":
migrateCmd(os.Args[2:])
default:
usage()
os.Exit(2)
}
}Cada subcomando tiene su propio flag.NewFlagSet. Para proyectos más grandes, bibliotecas como cobra o urfave/cli estructuran este esquema con ayuda generada automáticamente.
Ejercicios
Definimos flag -n de tipo int con valor predeterminado 1 y describimos 'ripetizioni', luego llamamos flag.Parse() y marcamos el valor.
Solución disponible después de 3 intentos
Si no hay ningún argumento posicional detrás de flag.Parse, estampa 'manca argumento' en Stderr y termina con os.Exit(1).
Solución disponible después de 3 intentos
¿Qué mensajes de error y uso transmiten en una CLI compatible con Unix?
fmt.Fprintln(???, "uso: prog ...")