Cada vez que ejecutas una operación de I/O directa (como un Read de un archivo o un Write a un socket), el procesador debe realizar una syscall, cambiando el contexto de tu programa al kernel. Este cambio de contexto es costoso en términos de ciclos de CPU. bufio soluciona esto mediante el buffering: introduce una capa de memoria intermedia en el espacio de usuario que actúa como acumulador.
Un bufio.Writer agrupa múltiples escrituras pequeñas en una única llamada al sistema cuando el buffer se llena, mientras que un bufio.Reader lee un bloque grande del kernel de golpe para que tú puedas extraer pedazos pequeños de él sin volver a invocar al sistema operativo. Si necesitas una API más ergonómica para procesar flujos de texto, bufio.Scanner es la opción estándar para segmentar datos por líneas o palabras. Sin embargo, debes saber cuándo retirarte: si ya estás trabajando con buffers grandes (por ejemplo, leyendo chunks de 64KB directamente), añadir bufio solo añade una copia de memoria innecesaria y latencia.
El uso de bufio es crítico cuando realizas muchas operaciones de I/O pequeñas, como escribir un carácter a la vez o leer una línea tras otra. Si lo usas correctamente, optimizarás el throughput de tu aplicación; si lo usas mal (olvidando el Flush o añadiendo capas de buffering innecesarias sobre buffers ya grandes), perderás datos o degradarás el rendimiento.
package main
import (
"bufio"
"bytes"
"fmt"
"strings"
)
func main() {
// Usamos bytes.Buffer como nuestro destino (simula un archivo o red)
var destino bytes.Buffer
// 1. Uso de bufio.Writer
// Agrupamos escrituras pequeñas para evitar múltiples syscalls.
writer := bufio.NewWriter(&destino)
datos := "Go es eficiente\ny bufio es clave\npara el rendimiento."
// Realizamos varias escrituras pequeñas.
// Estas se quedan en el buffer de memoria de 'writer' y no en 'destino' todavía.
for _, linea := range strings.Split(datos, "\n") {
_, err := writer.WriteString(linea + " ")
if err != nil {
fmt.Printf("Error escribiendo: %v\n", err)
return
}
}
// IMPORTANTE: Los datos residen en el buffer de memoria.
// Si no llamamos a Flush, los últimos bytes nunca llegarán a 'destino'.
if err := writer.Flush(); err != nil {
fmt.Printf("Error en flush: %v\n", err)
return
}
// 2. Uso de bufio.Scanner para lectura ergonómica
// Scanner es ideal para leer tokens (palabras, líneas) sin gestionar manualmente los bytes.
scanner := bufio.NewScanner(&destino)
// Cambiamos la estrategia de división: en lugar de líneas, leeremos palabras.
scanner.Split(bufio.ScanWords)
fmt.Println("Tokens detectados por el scanner:")
for scanner.Scan() {
// scanner.Text() extrae el token actual como string.
fmt.Printf("Token: [%s]\n", scanner.Text())
}
// Es buena práctica verificar si el scanner falló por un error de lectura,
// no solo porque terminó el stream.
if err := scanner.Err(); err != nil {
fmt.Printf("Error durante el escaneo: %v\n", err)
}
}
En el ejemplo anterior, hemos visto cómo writer encapsula a destino. Cuando llamamos a writer.WriteString, los datos se copian al buffer interno de bufio, no al bytes.Buffer original. Es la llamada a writer.Flush() la que realmente empuja esos bytes acumulados hacia el destino. Si ese Flush no se ejecutara, el contenido de destino estaría incompleto.
Para la lectura, hemos usado bufio.NewScanner. A diferencia de un Reader crudo, el Scanner mantiene un estado interno que le permite buscar delimitadores. Al usar scanner.Split(bufio.ScanWords), le estamos indicando que su lógica de “división” no debe buscar saltos de línea, sino espacios en blanco, permitiéndonos tokenizar el texto de forma muy sencilla. Si intentáramos hacer lo mismo con un bufio.Reader estándar, tendríamos que gestionar manualmente la búsqueda de espacios y la acumulación de bytes en un slice.
El error frecuente
El error más peligroso con bufio.Writer es olvidar llamar a .Flush().
// ERROR: Los datos se pierden
w := bufio.NewWriter(file)
w.WriteString("Datos críticos")
// El programa termina aquí sin llamar a w.Flush()
// "Datos críticos" solo vive en la memoria RAM del proceso y se pierde.
Como el Writer mantiene los datos en un buffer para optimizar el rendimiento, el flujo de datos se corta cuando el buffer no se ha llenado por completo. Si el proceso termina antes de que se dispare un llenado automático o una llamada manual a Flush(), los datos se pierden silenciosamente. Siempre asegúrate de llamar a Flush() antes de cerrar el archivo o la conexión.
N° 111