io y bufio en Go: Reader, Writer, Scanner, pipes y composición de streams

En Go, los paquetes io y bufio definen las interfaces y herramientas para trabajar con flujos de datos. Entender io.Reader, io.Writer y sus composiciones te permite escribir código que funciona igual con ficheros, conexiones de red, buffers en memoria y cualquier otra fuente de datos.

io.Reader y io.Writer

Las dos interfaces fundamentales tienen un solo método cada una. Cualquier tipo que las implemente se puede usar con todo el ecosistema de funciones de io:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Read llena el slice p y devuelve cuántos bytes leyó. Cuando ya no hay más datos devuelve io.EOF. Write escribe el slice completo o devuelve error.

io.Copy, io.ReadAll y io.LimitReader

package main

import (
    "io"
    "os"
    "strings"
)

func main() {
    // io.Copy: copia desde Reader a Writer sin buffer intermedio en memoria
    src := strings.NewReader("hola, mundo")
    io.Copy(os.Stdout, src) // hola, mundo

    // io.ReadAll: lee todo el contenido en memoria
    r := strings.NewReader("datos completos")
    datos, err := io.ReadAll(r)
    if err != nil {
        panic(err)
    }
    _ = datos

    // io.LimitReader: limita cuánto se puede leer (seguridad ante payloads enormes)
    limitado := io.LimitReader(os.Stdin, 1024*1024) // máx 1 MB
    io.Copy(os.Stdout, limitado)
}

bufio.Scanner: leer línea a línea

bufio.Scanner es la forma idiomática de leer texto línea a línea en Go. Admite cualquier io.Reader:

func contarLineas(r io.Reader) (int, error) {
    scanner := bufio.NewScanner(r)
    n := 0
    for scanner.Scan() {
        linea := scanner.Text() // línea sin el salto de línea
        _ = linea
        n++
    }
    return n, scanner.Err() // scanner.Err() no devuelve io.EOF
}

func main() {
    f, _ := os.Open("fichero.txt")
    defer f.Close()

    n, err := contarLineas(f)
    if err != nil {
        panic(err)
    }
    fmt.Printf("líneas: %dn", n)
}

bufio.NewWriter y Flush

bufio.Writer acumula escrituras pequeñas en un buffer interno y las envía al destino en bloques más grandes, reduciendo las llamadas al sistema:

func escribirFichero(nombre string, lineas []string) error {
    f, err := os.Create(nombre)
    if err != nil {
        return err
    }
    defer f.Close()

    w := bufio.NewWriter(f)
    for _, linea := range lineas {
        fmt.Fprintln(w, linea)
    }
    return w.Flush() // imprescindible: vacía el buffer al fichero
}

io.Pipe: conectar goroutines con streams

io.Pipe crea un par Reader/Writer sincronizados. Lo que escribe el Writer lo lee el Reader, sin buffer intermedio en memoria. Sirve para conectar etapas de procesado en goroutines distintas:

func comprimirYEnviar(datos []byte) error {
    pr, pw := io.Pipe()

    // Goroutine que comprime y escribe al pipe
    go func() {
        gz := gzip.NewWriter(pw)
        _, err := gz.Write(datos)
        gz.Close()
        pw.CloseWithError(err) // propaga el error al Reader
    }()

    // Esta goroutine sube el stream comprimido a S3
    _, err := s3.Upload(pr)
    return err
}

Composición de transformaciones

Las interfaces Reader y Writer permiten encadenar transformaciones sin copias intermedias:

func procesarFicheroGzip(nombre string) error {
    f, err := os.Open(nombre)
    if err != nil {
        return err
    }
    defer f.Close()

    // Cadena: fichero ? descomprimir gzip ? leer líneas con scanner
    gz, err := gzip.NewReader(f)
    if err != nil {
        return err
    }
    defer gz.Close()

    scanner := bufio.NewScanner(gz)
    for scanner.Scan() {
        procesar(scanner.Text())
    }
    return scanner.Err()
}

Leer y escribir con TeeReader

io.TeeReader duplica lo que se lee hacia un segundo Writer, útil para depurar o calcular checksums mientras se consume un stream:

func descargarYVerificar(url string) error {
    resp, _ := http.Get(url)
    defer resp.Body.Close()

    var checksumBuf bytes.Buffer
    h := sha256.New()

    // tee: todo lo que lea el scanner también va al hash
    tee := io.TeeReader(resp.Body, h)
    scanner := bufio.NewScanner(tee)
    for scanner.Scan() {
        _ = scanner.Text()
    }

    _ = checksumBuf
    fmt.Printf("SHA-256: %xn", h.Sum(nil))
    return scanner.Err()
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP