Un slice en Go no es un contenedor de datos por sí mismo, sino un encabezado (un descriptor) que apunta a un array subyacente (backing array). Este encabezado tiene tres campos fundamentales: un puntero al array, la longitud (len) y la capacidad (cap). Cuando llamas a append, Go no siempre crea un nuevo array; su comportamiento depende de la relación entre len y cap. Si el array subyacente tiene espacio suficiente (len < cap), Go simplemente escribe el nuevo elemento en la siguiente posición de memoria y actualiza la longitud. Sin embargo, si el slice está lleno (len == cap), ocurre una realocación: el runtime busca un nuevo bloque de memoria más grande, copia los elementos existentes al nuevo array, añade el nuevo elemento y devuelve un nuevo encabezado con el nuevo puntero y la nueva capacidad.
Esta lógica es la razón por la cual es obligatorio reasignar el slice: s = append(s, x). Como la realocación cambia la dirección de memoria del array subyacente, la variable original se quedaría apuntando al array viejo (que ahora es más pequeño o incluso ha sido liberado), dejando el encabezado desactualizado. En cuanto a la estrategia de crecimiento, Go ha optimizado cómo escala la capacidad para evitar desperdicios. Mientras que en slices pequeños suele duplicar la capacidad, en versiones recientes [disponible desde Go 1.18], el factor de crecimiento se suaviza cuando el slice es grande para evitar saltos de memoria excesivos, pasando de un factor de 2x a uno más cercano a 1.25x.
package main
import (
"fmt"
"unsafe"
)
func main() {
// Creamos un slice con len 2 y cap 2
s := []int{10, 20}
fmt.Printf("Inicial: len=%d cap=%d ptr=%p\n", len(s), cap(s), getPtr(s))
// Caso 1: Append sin realocación (si tuviéramos capacidad extra)
// Pero como cap=2, el siguiente append FORZARÁ una realocación.
s = append(s, 30)
fmt.Printf("Tras append (realocación): len=%d cap=%d ptr=%p\n", len(s), cap(s), getPtr(s))
// Caso 2: El crecimiento de capacidad
// Al añadir un elemento, Go suele duplicar la capacidad inicial de 2 a 4.
s = append(s, 40)
fmt.Printf("Tras append (sin realocación): len=%d cap=%d ptr=%p\n", len(s), cap(s), getPtr(s))
// Caso 3: Concatenación de dos slices usando el operador variadic ...
extra := []int{50, 60, 70}
s = append(s, extra...)
fmt.Printf("Tras concatenar: len=%d cap=%d ptr=%p\n", len(s), cap(s), getPtr(s))
// Demostramos que el puntero ha cambiado físicamente en memoria
printDetails(s)
}
// getPtr devuelve la dirección de memoria del primer elemento del backing array
func getPtr(s []int) uintptr {
return uintptr(unsafe.Pointer(&s[0]))
}
func printDetails(s []int) {
fmt.Printf("\nDetalles finales de memoria:\n")
fmt.Printf("Contenido: %v\n", s)
fmt.Printf("Dirección base: %p\n", &s[0])
fmt.Printf("Capacidad actual: %d\n", cap(s))
}
En el código anterior, fíjate en cómo la dirección de memoria (el ptr que imprimimos con %p) cambia drásticamente cuando ejecutamos el primer append(s, 30). Esto sucede porque el slice original tenía una capacidad de 2 y ya estaba lleno; Go tuvo que buscar un nuevo bloque de memoria para albergar el tercer elemento. En cambio, al añadir 40, la capacidad había crecido (probablemente a 4), por lo que el append simplemente escribió el 40 en la posición de memoria que ya tenía reservada, y el puntero se mantuvo idéntico. Finalmente, el uso de extra... permite “desempaquetar” el slice extra para que sus elementos se traten como argumentos individuales, permitiendo la concatenación eficiente en una sola operación.
El error frecuente
Un error clásico es ignorar el valor de retorno de append cuando se trabaja con la misma variable.
// ERROR: El slice 's' no se actualiza
s := []int{1, 2}
append(s, 3)
fmt.Println(s) // Imprime [1 2], el 3 se "perdió" para esta variable
Si append provoca una realocación, el nuevo array reside en una dirección de memoria distinta. Al no asignar el resultado a s, tu variable s sigue apuntando al array original (la dirección antigua), que ahora tiene una longitud de 2. El elemento 3 se escribió en una zona de memoria que el slice s ya no considera parte de su rango válido, lo que genera comportamientos erráticos o datos “desaparecidos”.
N° 48