Un slice en Go es una estructura de bajo nivel que consiste en un puntero a un array subyacente, una longitud (len) y una capacidad (cap). Cuando realizas un append y el número de elementos supera la capacidad actual, el runtime de Go debe buscar un nuevo array más grande en el heap, copiar todos los elementos existentes al nuevo espacio y actualizar el puntero. Este proceso de realocación consume ciclos de CPU y genera basura que el Garbage Collector (GC) tendrá que limpiar después.
Para evitar este overhead, puedes usar la función make para definir una capacidad inicial mayor a la longitud: make([]T, 0, n). Esto reserva espacio para n elementos de forma inmediata, permitiendo que los append posteriores simplemente modifiquen la memoria ya asignada sin disparar nuevas alocaciones. Debes usar esto siempre que conozcas de antemano el tamaño final de tu colección, como al procesar una lista de registros de una base de datos o serializar un buffer de bytes. Si el tamaño es totalmente desconocido, no intentes adivinar con números arbitrarios, ya que una mala estimación solo desperdiciará memoria innecesariamente. Si ignoras la pre-alocación en bucles intensivos, lo que romperás será el rendimiento de tu servicio, provocando latencias erráticas y un uso excesivo de la CPU por la presión sobre el recolector de basura.
package main
import (
"fmt"
)
type User struct {
ID int
Name string
Email string
}
// procesarUsuarios simula la carga de datos desde una fuente externa
func procesarUsuarios(n int) []User {
// Pre-alocamos con longitud 0 pero capacidad n.
// Esto es clave: si pusiéramos length en n, el slice
// ya contendría n elementos con valores zero.
usuarios := make([]User, 0, n)
for i := 1; i <= n; i++ {
// El append aquí es extremadamente eficiente porque
// no hay realocaciones; solo se escribe en la memoria
// ya reservada por make.
usuarios = append(usuarios, User{
ID: i,
Name: fmt.Sprintf("Usuario %d", i),
Email: fmt.Sprintf("user%d@ejemplo.com", i),
})
}
return usuarios
}
func main() {
const totalRegistros = 5
// Caso ideal: Conocemos el tamaño exacto
resultado := procesarUsuarios(totalRegistros)
fmt.Printf("Resultado: %+v\n", resultado)
fmt.Printf("Longitud (len): %d\n", len(resultado))
fmt.Printf("Capacidad (cap): %d\n", cap(resultado))
// Verificación de la capacidad para asegurar que no hubo re-alocaciones
if cap(resultado) == totalRegistros {
fmt.Println("Optimización exitosa: No se requirieron realocaciones.")
}
}
En el código anterior, la función procesarUsuarios recibe un parámetro n que representa la cantidad de elementos que esperamos manejar. Al ejecutar make([]User, 0, n), estamos inicializando el slice con una longitud de 0 (no hay elementos listos para ser leídos) pero con una capacidad de n (ya tenemos espacio reservado en memoria para n estructuras User).
Cuando el bucle for ejecuta append(usuarios, ...), el runtime simplemente incrementa el contador de len y escribe la nueva estructura User en la posición de memoria correspondiente. Como la capacidad es exactamente la que necesitamos, no se activa la lógica de redimensionamiento del slice. Al final, en main, al imprimir cap(resultado), verás que es igual a totalRegistros, confirmando que el slice se mantuvo estable en una única asignación de memoria.
El error frecuente
Un error clásico ocurre al confundir la longitud con la capacidad al inicializar el slice. Si intentas “pre-alocar” usando el primer argumento de make, cometes un error de lógica:
// ERROR COMÚN
n := 3
s := make([]int, n) // len=3, cap=3, valores: [0, 0, 0]
for i := 0; i < n; i++ {
s = append(s, i) // El append añade AL FINAL de la longitud actual
}
fmt.Println(s) // Resultado: [0, 0, 0, 0, 1, 2]
En este caso, make([]int, n) crea un slice que ya tiene n elementos inicializados en cero. Al hacer append, Go no sobreescribe esos ceros, sino que los añade después, resultando en un slice con tamaño 2n. La forma correcta es siempre make([]T, 0, n) si tu intención es usar append.
N° 51