En Go, el Garbage Collector (GC) se encarga de la gestión automática de la memoria, lo que significa que no tienes que llamar manualmente a free() o delete. El GC rastrea todos los punteros activos en la memoria y, cuando un objeto ya no es alcanzable desde las “raíces” (como variables en la pila o variables globales), el recolector lo marca para ser liberado.
Para que esto sea eficiente, el compilador y el runtime cooperan estrechamente: el compilador genera metadatos sobre el layout (la forma) de cada tipo. Esto permite que el GC sepa exactamente en qué bytes de una estructura reside un puntero que debe ser rastreado y en qué bytes hay datos simples (como un int) que puede ignorar.
Debes usar punteros cuando necesites mutabilidad o quieras evitar copiar estructuras grandes, pero debes ser consciente de la densidad de punteros. Un slice de punteros ([]*User) obliga al GC a seguir cada dirección de memoria individualmente, lo que aumenta la carga de escaneo. En cambio, un slice de valores ([]User) es un bloque de memoria contiguo que el GC procesa mucho más rápido. Si creas una enorme cantidad de objetos pequeños esparcidos en el heap con muchos punteros, aumentarás la presión sobre el GC y las latencias de las pausas de detención del mundo (Stop The World).
Aunque Go no tiene destructores deterministas como C++, lo que permite un modelo de concurrencia más simple y predecible, el uso de defer es el patrón estándar para la limpieza de recursos no relacionados con la memoria, como archivos o conexiones de red.
package main
import (
"fmt"
"os"
)
// Session simula un recurso que requiere limpieza manual.
type Session struct {
ID int
File *os.File
}
// Close libera el recurso físico. Es el equivalente lógico a un destructor.
func (s *Session) Close() {
fmt.Printf("Cerrando sesión %d y su archivo en %s\n", s.ID, s.File.Name())
s.File.Close()
}
func runSession(id int, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
// Go no tiene destructores. Para recursos que dependen del sistema operativo
// (como archivos), usamos 'defer' para garantizar que se liberen al salir
// de la función, sin importar si hay un error o un panic.
session := &Session{ID: id, File: f}
defer session.Close()
fmt.Printf("Trabajando en sesión %d...\n", id)
return nil
}
func main() {
// 1. Uso de defer para limpieza determinística de recursos externos.
_ = runSession(1, "session_1.log")
// 2. Comparativa de densidad de punteros y presión sobre el GC.
const size = 100_000
// Opción A: Alta densidad de punteros.
// El GC debe seguir 100,000 direcciones de memoria distintas.
ptrs := make([]*int, size)
for i := 0; i < size; i++ {
val := i
ptrs[i] = &val
}
// Opción B: Baja densidad de punteros (más eficiente para el GC).
// El GC ve un único bloque contiguo de enteros; no hay punteros que seguir.
vals := make([]int, size)
for i := 0; i < size; i++ {
vals[i] = i
}
fmt.Printf("Finalizado: procesados %d elementos en cada modo.\n", size)
}
Análisis del ejemplo
En la función runSession, observamos cómo manejamos un recurso del sistema operativo. Como la memoria gestionada por el GC no incluye automáticamente el cierre de un archivo, utilizamos defer session.Close(). Esto asegura que, independientemente de cómo termine la función, el descriptor de archivo se libere, evitando fugas de recursos (resource leaks).
En el main, la diferencia entre ptrs y vals es crítica para el rendimiento. En ptrs, hemos creado un slice que contiene 100,000 punteros. Cada puntero apunta a una dirección de memoria diferente en el heap. Cuando el GC se ejecute, tendrá que realizar 100,000 saltos de memoria para verificar si esos enteros siguen siendo válidos. En el caso de vals, el slice es un bloque de memoria compacto que contiene los valores directamente. El GC puede escanear este bloque de forma extremadamente eficiente porque no necesita “saltar” a otras direcciones para encontrar los datos.
El error frecuente
A menudo se intenta usar runtime.SetFinalizer para gestionar la limpieza de recursos (como cerrar un archivo) cuando el objeto es recolectado por el GC. Esto es un error de diseño peligroso. Los finalizadores son no-determinísticos: no tienes garantía de cuándo se ejecutarán, ni siquiera de que se ejecutarán antes de que el programa termine. Si dependes de un finalizador para cerrar archivos o liberar sockets, podrías agotar los descriptores de archivo del sistema operativo antes de que el GC decida pasar por el objeto, provocando errores de “too many open files” aunque no tengas fugas de memoria. Usa siempre defer para limpieza de recursos.
N° 67