io.Copy es la herramienta estándar en Go para mover datos desde un io.Reader hacia un io.Writer. A diferencia de otras aproximaciones que intentan cargar todo el contenido en la memoria RAM, io.Copy utiliza un buffer interno de 32KB para transferir fragmentos de datos de forma iterativa. Esto garantiza que el consumo de memoria se mantenga constante ($O(1)$), permitiéndote procesar archivos de terabytes o streams infinitos con apenas unos pocos kilobytes de RAM. Debes usarlo cuando el objetivo sea el transporte puro de bytes de un punto A a un punto B sin necesidad de manipular los datos en el camino. Si intentas leer un archivo masivo con io.ReadAll, el programa intentará asignar un slice con el tamaño total del archivo, lo que provocará un error de out of memory (OOM) y hará colapsar tu servicio. Si necesitas limitar la transferencia, usa io.CopyN; si estás en un bucle de alto rendimiento y quieres evitar la presión sobre el recolector de basura (GC) por asignaciones constantes, usa io.CopyBuffer.
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
func main() {
// Simulamos una fuente de datos (podría ser un archivo o una conexión TCP)
fuente := strings.NewReader("HEADER:DATA_PAYLOAD_EXTREMADAMENTE_LARGO")
var destino bytes.Buffer
// 1. io.CopyN: Copiamos exactamente 6 bytes (el "header")
// Útil cuando conoces el tamaño de un encabezado antes del cuerpo.
n, err := io.CopyN(&destino, fuente, 6)
if err != nil {
fmt.Printf("Error al copiar header: %v\n", err)
return
}
fmt.Printf("Copiado header: %d bytes. Estado buffer: %s\n", n, destino.String())
// 2. io.CopyBuffer: Copiamos el resto usando un buffer pre-asignado
// En aplicaciones de alto rendimiento, reutilizamos el buffer para evitar
// que el GC trabaje de más con asignaciones en el heap.
buf := make([]byte, 8192) // Buffer de 8KB personalizado
n, err = io.CopyBuffer(&destino, fuente, buf)
if err != nil {
fmt.Printf("Error en la copia principal: %v\n", err)
return
}
fmt.Printf("Copiado payload: %d bytes. Total: %s\n", n, destino.String())
// 3. El patrón io.Discard: Drenar cuerpos de respuesta HTTP
// Es vital drenar el cuerpo de una respuesta incluso si no te interesa
// el contenido, para permitir que la conexión TCP se reutilice (Keep-Alive).
respBody := strings.NewReader("Cuerpo de respuesta HTTP que debe ser drenado")
_, err = io.Copy(io.Discard, respBody)
if err != nil {
fmt.Printf("Error drenando: %v\n", err)
return
}
fmt.Println("Drenaje completado: la conexión está lista para ser reutilizada.")
}
En el código anterior, fíjate cómo io.CopyN limita la lectura exactamente a los primeros 6 bytes del fuente, lo cual es fundamental cuando el protocolo requiere leer un prefijo de tamaño fijo. Para la parte más pesada de la transferencia, utilizamos io.CopyBuffer pasando nuestro propio slice buf. Al pasarle un buffer ya creado, le estamos diciendo al runtime: “no asignes uno nuevo cada vez que llames a esta función; usa este que ya tengo”. Esto reduce drásticamente la fragmentación de memoria en procesos de alta carga.
Finalmente, observa el uso de io.Discard junto a io.Copy. En el contexto de clientes HTTP, si obtienes una respuesta pero decides que no necesitas el cuerpo, no puedes simplemente abandonar el objeto resp.Body. Si no “drenas” los bytes restantes usando io.Copy(io.Discard, resp.Body), la conexión subyacente permanecerá ocupada y no podrá volver al pool de conexiones, agotando los sockets de tu sistema operativo.
El error frecuente
// MAL: Esto causará un crash (OOM) si el archivo es de 50GB
// y tu contenedor solo tiene 4GB de RAM.
data, err := io.ReadAll(archivoGigante)
if err != nil {
return err
}
err = os.WriteFile("copia.dat", data, 0644)
El error es intentar materializar el flujo de datos en un slice de bytes completo. io.ReadAll es cómodo para archivos de configuración pequeños, pero es peligroso para cualquier flujo de datos cuya longitud sea desconocida o superior a la memoria disponible.
N° 109