# Domina las Entradas y Salidas (I/O) en Go: De la Terminal al Manejo de Archivos
Tabla de Contenidos
🧩 Módulo 1, Capitulo 4: Domina las Entradas y Salidas (I/O) en Go: De la Terminal al Manejo de Archivos
Mis Bros!!! En el capitulo anterior sentamos las bases, pero ahora vamos a meternos en la sala de máquinas. Entender cómo Go maneja las entradas y salidas (I/O) no solo te hará mejor programador, sino que te permitirá escribir código más eficiente y robusto, especialmente cuando trabajes con archivos grandes o necesites un control más fino. Estaremos conociendo:
- Cómo leer argumentos desde la línea de comandos.
- El poder de
bufio: qué es un buffer y cómo leer por líneas, palabras o hasta bytes. - Estrategias para leer y escribir archivos: cuándo cargar todo en memoria y cuándo usar streams.
- Buenas prácticas como el manejo de errores con contexto y verificar si un archivo existe.
1. La Terminal: Tu Primera Puerta de Entrada!!!
Antes de pedirle al usuario que escriba algo, a menudo tu programa necesitará que le pasen información desde el momento en que se ejecuta. Para eso están los argumentos de línea de comandos.
1.1. Argumentos de Línea de Comandos con os.Args
Cuando ejecutas go run cesarlead.go argumento1 argumento2, el paquete os captura esa información en un slice de
strings llamado os.Args.
os.Args[0]es siempre la ruta del programa que se está ejecutando.os.Args[1:]contiene todos los argumentos que le pasaste.
Ejemplo:
package main
import ( "fmt" "os")
func main() { // os.Args es un slice de strings con los argumentos argumentos := os.Args
fmt.Printf("Mi programa se llama: %s\n", argumentos[0])
// Verificamos si nos pasaron argumentos adicionales if len(argumentos) > 1 { fmt.Println("Me pasaste los siguientes argumentos:") // Recorremos solo los argumentos, ignorando el nombre del programa for _, arg := range argumentos[1:] { fmt.Printf("- %s\n", arg) } } else { fmt.Println("No me pasaste ningún argumento, pana.") }}Para probarlo:
go run cesarlead.go hola mundo 123
Salida:
Mi programa se llama: cesarlead.goMe pasaste los siguientes argumentos:- hola- mundo- 1231.2. Lectura Interactiva: El Poder de bufio
Cuando necesitas que el usuario escriba mientras el programa corre (Esas tipicas herramientas de CLI que normalmente
construimos para automatizar tareas o si eres como yo, que prefiera algo mas retro hahaha), bufio es el rey. Pero, qué
hace realmente???
Qué pasa por debajo con bufio?
Imagina que leer de la terminal o de un disco es como ir a un pozo de agua a buscar un vaso. Hacer un viaje por cada vaso es muy ineficiente. Una operación de I/O (como leer del disco) implica una “llamada al sistema” (syscall), que es una operación costosa donde tu programa le pide un favor al sistema operativo.
Un buffer es como un balde. Con bufio, Go hace un solo viaje al pozo (una sola syscall) para llenar el balde (el
buffer, que es un []byte en memoria) y luego te va dando vasos de agua (los datos) desde el balde, lo cual es cono más
rápido.
El bufio.Scanner es una herramienta inteligente que se sienta sobre ese buffer y sabe cómo cortar los datos.
Controlando cómo lee el Scanner:
Por defecto, el scanner corta por líneas (\n), pero puedes decirle que corte por palabras.
Ejemplo 1: Leyendo Líneas Completas (comportamiento por defecto)
package main
import ( "bufio" "fmt" "os" "strings")
func leerLineas() { scanner := bufio.NewScanner(os.Stdin) fmt.Println("Escribe frases completas. Escribe 'chao' para salir.")
for scanner.Scan() { linea := scanner.Text() if strings.ToLower(linea) == "chao" { break } fmt.Printf("Recibí la línea: '%s'\n", linea) }
if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "Error leyendo la entrada:", err) }}
// Para llamar a esta función, pon leerLineas() en tu func main()Ejemplo 2: Leyendo Palabra por Palabra
package main
import ( "bufio" "fmt" "os")
func leerPalabras() { scanner := bufio.NewScanner(os.Stdin)
// Aquí está la magia: cambiamos la función de "corte" scanner.Split(bufio.ScanWords)
fmt.Println("Escribe algo y verás cómo lo separo por palabras. ")
fmt.Println("Palabras recibidas:") for scanner.Scan() { palabra := scanner.Text() fmt.Printf("- %s\n", palabra) }
if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "Error leyendo la entrada:", err) }}
// Para llamar a esta función, pon leerPalabras() en tu func main()** Puntos Clave:**
-
os.Args: Para argumentos al inicio del programa. -
Buffer: Un espacio en memoria para reducir las costosas llamadas al sistema y aumentar la eficiencia.
-
bufio.Scanner: Una herramienta de alto nivel para leer datos desde un buffer. -
scanner.Split(): Te da el control para decidir si quieres leer por líneas, palabras, caracteres (bufio.ScanRunes) o hasta definir tu propio patrón de corte.
2. Archivos: El Corazón del Almacenamiento
Aquí es donde se pone interesante. Dependiendo del tamaño del archivo, tienes dos estrategias principales.
Estrategia 1: Leer Todo de una Vez (Para archivos pequeños/medianos)
Si sabes que el archivo no es gigantesco (unos pocos megabytes como máximo), os.ReadFile es la opción más sencilla y
limpia. Carga todo el contenido del archivo en un slice de bytes ([]byte) en memoria.
Ejercicio: Procesar un JSON de Configuración
Imagina un archivo config.json para tu aplicación:
{ "version": "1.2.0", "puerto_servidor": 8080, "caracteristicas_activas": [ "login_v2", "dashboard_beta", "modo_oscuro" ]}El Código Go:
package main
import ( "encoding/json" "fmt" "os")
type Configuracion struct { Version string `json:"version"` Puerto int `json:"puerto_servidor"` Features []string `json:"caracteristicas_activas"`}
func cargarConfig(ruta string) (Configuracion, error) { var config Configuracion
// Leemos todo el archivo en memoria bytes, err := os.ReadFile(ruta) if err != nil { // Error con contexto!!! Así sabemos dónde y por qué falló. return config, fmt.Errorf("error al leer el archivo de configuración '%s': %w", ruta, err) }
// Hacemos el unmarshal err = json.Unmarshal(bytes, &config) if err != nil { return config, fmt.Errorf("error al parsear el JSON de '%s': %w", ruta, err) }
return config, nil}
func main() { config, err := cargarConfig("config.json") if err != nil { fmt.Println("¡FATAL! No se pudo cargar la configuración:", err) os.Exit(1) // Salimos del programa con un código de error }
fmt.Printf("Configuración cargada (v%s)\n", config.Version) fmt.Printf("El servidor correrá en el puerto: %d\n", config.Puerto) fmt.Printf("Características activas: %v\n", config.Features)}** Buena Práctica - Errores con Contexto:** Usar fmt.Errorf con el verbo %w “envuelve” el error original. Esto crea
una cadena de errores que te permite saber exactamente dónde ocurrió el problema, algo invaluable para el debugging.
Estrategia 2: Streaming (Para Archivos Gigantes)
Muchachones si intentan cargar un archivo de 5 GB en memoria con os.ReadFile, el programara seguramente se tirara 3,
por falta de RAM. La solución es el streaming: procesar el archivo pedazo por pedazo, sin necesidad de cargarlo
todo.
Para esto, volvemos a nuestro amigo bufio.Scanner, pero esta vez apuntando a un archivo.
Ejercicio: Analizar un Archivo de Log
Imagina un server.log de miles de líneas:
[INFO] 2025-07-15 10:30:00 - Servicio iniciado.[DEBUG] 2025-07-15 10:31:15 - Conexión entrante de 192.168.1.10.[ERROR] 2025-07-15 10:32:05 - Fallo al conectar con la base de datos.[INFO] 2025-07-15 10:33:00 - Operación completada por el usuario 'admin'.[ERROR] 2025-07-15 10:34:22 - Timeout esperando respuesta del servicio de pagos.El Código Go:
package main
import ( "bufio" "fmt" "os" "strings")
// procesarLog lee un archivo grande línea por línea y cuenta las que contienen "ERROR"func procesarLog(ruta string) (int, error) { // Primero, verificamos si el archivo existe if _, err := os.Stat(ruta); os.IsNotExist(err) { return 0, fmt.Errorf("el archivo de log '%s' no existe", ruta) }
file, err := os.Open(ruta) if err != nil { return 0, fmt.Errorf("error al abrir el log '%s': %w", ruta, err) } defer file.Close() // No olvides cerrar el archivo por amor a Crito!!!
scanner := bufio.NewScanner(file) contadorErrores := 0 numeroLinea := 0
fmt.Printf("Analizando '%s'...\n", ruta)
for scanner.Scan() { numeroLinea++ linea := scanner.Text() if strings.Contains(linea, "[ERROR]") { contadorErrores++ fmt.Printf(" -> Error encontrado en la línea %d: %s\n", numeroLinea, linea) } }
if err := scanner.Err(); err != nil { return 0, fmt.Errorf("error al escanear el archivo '%s': %w", ruta, err) }
return contadorErrores, nil}
func main() { // Puedes crear el archivo 'server.log' para probar errores, err := procesarLog("server.log") if err != nil { fmt.Println("Hubo un problema:", err) os.Exit(1) } fmt.Printf("\nAnálisis completo. Se encontraron %d errores.\n", errores)}** Buena Práctica - Verificar Existencia:** Usar os.Stat junto con os.IsNotExist(err) es la forma idiomática en Go
para comprobar si un archivo o directorio existe antes de intentar usarlo.
3. Escritura de Archivos: Dejando tu Huella
Al igual que con la lectura, tenemos dos estrategias para escribir.
Estrategia 1: Escribir Todo de una Vez
Para datos pequeños, os.WriteFile es perfecto. Crea el archivo (o lo sobrescribe si ya existe) y escribe todo el
contenido de un []byte de una sola vez.
Estrategia 2: Streaming con bufio.Writer
Para escribir grandes cantidades de datos, usar un bufio.Writer es mucho más eficiente. Funciona a la inversa que el
Scanner: en lugar de hacer una llamada al sistema por cada Write(), acumula los datos en su buffer y los escribe en
el disco de golpe cuando el buffer se llena o cuando le dices explícitamente que lo haga con Flush().
Muy importante muchachones, si no llaman a Flush(), puede que parte de los datos (o todos) se queden en el buffer y
nunca se escriban en el archivo.
Ejercicio Combinado: Filtrar y Escribir
Vamos a leer el usuarios.json del artículo anterior, filtrar solo los usuarios activos, y escribir sus nombres en un
nuevo archivo activos.txt usando un bufio.Writer.
package main
import ( "bufio" "encoding/json" "fmt" "os")
type Usuario struct { ID int `json:"id"` Nombre string `json:"nombre"` Activo bool `json:"activo"` Email string `json:"email"`}
func filtrarYGuardarActivos(origenJson, destinoTxt string) error { // 1. Leer el archivo JSON de origen (es pequeño, usamos ReadFile) bytes, err := os.ReadFile(origenJson) if err != nil { return fmt.Errorf("no se pudo leer el JSON de origen: %w", err) }
var usuarios []Usuario if err := json.Unmarshal(bytes, &usuarios); err != nil { return fmt.Errorf("JSON malformado: %w", err) }
// 2. Crear el archivo de destino para escribir file, err := os.Create(destinoTxt) if err != nil { return fmt.Errorf("no se pudo crear el archivo de destino: %w", err) } defer file.Close()
// 3. Crear un Writer con buffer para escribir eficientemente writer := bufio.NewWriter(file) defer writer.Flush() // SÚPER IMPORTANTE!!! Defer para asegurar que se escribe todo al final.
fmt.Fprintf(writer, "--- Usuarios Activos ---\n") contador := 0 for _, u := range usuarios { if u.Activo { // Escribimos al buffer, no directamente al disco _, err := fmt.Fprintf(writer, " - %s (%s)\n", u.Nombre, u.Email) if err != nil { return fmt.Errorf("error escribiendo en el buffer: %w", err) } contador++ } }
fmt.Printf("Se escribieron %d usuarios activos en '%s'\n", contador, destinoTxt) return nil}
func main() { // Necesitas un archivo 'usuarios.json' para que esto funcione err := filtrarYGuardarActivos("usuarios.json", "activos.txt") if err != nil { fmt.Println("Falló la operación:", err) }}** Puntos Clave:**
os.Create(): Crea un archivo para escritura (o lo trunca si existe).bufio.NewWriter(): Crea un escritor con buffer para I/O eficiente.writer.Flush(): ¡La llamada más importante! Fuerza la escritura de cualquier dato que quede en el buffer al disco. Usardeferes la forma más segura de no olvidarlo.
Muchachones, ya sabemos crear y sobrescribir archivos, pero hay un escenario clave que no hemos cubierto. Qué pasa si
solo queremos añadir una nueva línea a nuestro archivo de log sin borrar todo lo que ya estaba??? Si usas
os.Create() o os.WriteFile(), vas a sobrescribir el archivo completo. No queremos eso!
Para esta misión, necesitamos una herramienta más poderosa: os.OpenFile(). Esta función nos da control total sobre
cómo abrimos el archivo.
4. Escritura Avanzada: Añadir a un Archivo Existente (Append)
La clave para añadir contenido (hacer append) está en usar las “banderas” (flags) correctas al abrir el archivo. Piénsalo como darle instrucciones específicas al sistema operativo.
Cómo funciona os.OpenFile?
La firma de la función es: os.OpenFile(name string, flag int, perm FileMode) (*File, error)
-
name: La ruta del archivo, igual que siempre. -
flag: Aquí está la magia. Es un entero que le dice a Go cómo queremos abrir el archivo. Podemos combinar varias banderas usando el operador|(OR a nivel de bits). Las más importantes para nosotros son:-
os.O_APPEND: La estrella del show. Le dice al sistema operativo: “escribe siempre al final del archivo”. -
os.O_WRONLY: Abrir el archivo solo para escritura (Write-Only). -
os.O_CREATE: Si el archivo no existe, créalo. Esto hace nuestro código más robusto.
-
-
perm: Los permisos del archivo si es que se necesita crear. Un valor común es0644, que significa que el dueño del archivo puede leer y escribir, mientras que los demás solo pueden leer.
Ejercicio: Agregar Entradas a un Log Existente
Vamos a tomar nuestro ejemplo del server.log y a crear una función que añada nuevas entradas de log cada vez que la
llamemos, sin borrar las anteriores.
El Código Go:
package main
import ( "fmt" "os" "time")
// agregarEntradaLog añade una línea de texto al final del archivo especificado.func agregarEntradaLog(ruta string, nivel string, mensaje string) error { // La magia está en estas "banderas" (flags). // os.O_APPEND: Añade los datos al final del archivo. // os.O_CREATE: Crea el archivo si no existe. // os.O_WRONLY: Abrir solo para escritura. flags := os.O_APPEND | os.O_CREATE | os.O_WRONLY
// Abrimos el archivo con las flags y permisos especificados. file, err := os.OpenFile(ruta, flags, 0644) if err != nil { return fmt.Errorf("error al abrir/crear el log '%s': %w", ruta, err) } defer file.Close() // ¡Indispensable cerrar el archivo!
// Creamos la línea de log con timestamp. timestamp := time.Now().Format("2006-01-02 15:04:05") lineaLog := fmt.Sprintf("[%s] %s - %s\n", nivel, timestamp, mensaje)
// Escribimos la cadena de bytes en el archivo. // Usamos WriteString para eficiencia, ya que no necesitamos el buffer de bufio // para una sola operación de escritura. if _, err := file.WriteString(lineaLog); err != nil { return fmt.Errorf("error al escribir en el log '%s': %w", ruta, err) }
return nil}
func main() { archivoLog := "server.log"
fmt.Printf("Añadiendo nuevas entradas al log '%s'...\n", archivoLog)
// Simulamos varias operaciones que registran en el log. err := agregarEntradaLog(archivoLog, "INFO", "El sistema se está iniciando.") if err != nil { fmt.Println("Error:", err) }
time.Sleep(1 * time.Second) // Pequeña pausa para que cambie el timestamp
err = agregarEntradaLog(archivoLog, "WARN", "La memoria está al 85% de su capacidad.") if err != nil { fmt.Println("Error:", err) }
time.Sleep(1 * time.Second)
err = agregarEntradaLog(archivoLog, "SUCCESS", "Tarea de respaldo completada exitosamente.") if err != nil { fmt.Println("Error:", err) }
fmt.Println("¡Entradas agregadas! Revisa el contenido del archivo 'server.log'.")}Para probarlo:
-
Ejecuta el programa una vez:
go run tu_programa.go. -
Revisa el archivo
server.log. Verás las tres líneas que se agregaron. -
Vuelve a ejecutar el programa.
-
Revisa
server.logde nuevo. Verás que las tres nuevas líneas se añadieron al final, sin borrar las anteriores!
Puntos Clave:
* **`os.OpenFile()`:** Tu navaja suiza para abrir archivos con control total. Es la función que debes usar cuando `os.Create()` o `os.ReadFile()` no son suficientes.
* **`os.O_APPEND`:** La bandera crucial para añadir contenido sin sobrescribir. ¡No la olvides\!
* **Combinar flags con `|`:** Es la forma idiomática en Go para unir múltiples opciones de configuración.
* **Robustez:** Usar `os.O_CREATE` junto con `os.O_APPEND` hace que tu código funcione tanto si el archivo ya existe como si no.Bueno muchachones, hasta el proximo capitulo, no dejen de practicar, ese es el secreto para ser un buen programador, practicar, practicar y seguir practicando. Activitos!!!