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()
}
