Introducción: ¿Por qué «simplemente struct» no es tan simple?
\n\n¿Alguna vez has escrito en Go y, mirando tu programa, te has preguntado: «¿Dónde vive ahora mi estructura? ¿En la rápida y acogedora pila o en los vastos espacios del montón bajo la supervisión del recolector de basura?». Esta pregunta atormenta no solo a los principiantes. En Stack Overflow, el tema «Stack vs heap allocation of structs in Go, and how they relate to garbage collection» ha acumulado miles de visitas. Y no es casualidad.
\n\nLa aparente simplicidad de Go oculta un poderoso mecanismo: el escape analysis. Es un detective del compilador que decide el destino de cada objeto. De su decisión depende el rendimiento de tu aplicación y la carga sobre el GC (Garbage Collector). En este tutorial paso a paso, nos pondremos el sombrero de detective y analizaremos:
\n- \n
- ¿Por qué una estructura vive en la pila y otra en el montón? \n
- Cómo los punteros y los cierres rompen la idílica pila. \n
- Cómo este conocimiento te ayudará a escribir código rápido y eficiente en Go. \n
Pila vs Montón: Breve introducción
\nAntes de sumergirnos en el código, refresquemos la teoría. No te preocupes, será rápido y directo.
\n\nPila (Stack)
\n- \n
- Velocidad: Relámpago. Asignar memoria es simplemente mover el puntero hacia abajo. \n
- Ciclo de vida: Estrictamente ligado a la llamada de función. Vive mientras la función está activa. \n
- Gestión: Automática. Se «quema» al salir de la función. \n
- Tamaño: Pequeño y fijo por gorutina (normalmente 2-4 KB). \n
Montón (Heap)
\n- \n
- Velocidad: Más lento. Requiere buscar un bloque de memoria libre. \n
- Ciclo de vida: Mientras haya referencias al objeto desde el programa. Puede sobrevivir a la función que lo creó. \n
- Gestión: Recolector de basura (GC). Escanea el montón, encuentra objetos «muertos» y libera memoria. Esto consume recursos de CPU. \n
- Tamaño: Grande (limitado solo por la memoria del SO). \n
La regla de oro de Go: «Mantén los datos en la pila el mayor tiempo posible». Esto reduce la carga sobre el GC y acelera el programa.
\n\nEscape Analysis: El detective principal de Go
\nEn Go, no escribes new o malloc para indicar explícitamente dónde colocar un objeto. El compilador decide esto por ti. Este proceso se llama escape analysis (análisis de escape).
Observa tu código y se hace una pregunta: «¿Puede una referencia a este objeto salir de la función actual?».
\n- \n
- Si NO (el objeto no escapa) → El objeto se coloca en la pila. \n
- Si SÍ (el objeto escapa) → El objeto se coloca en el montón. \n
Veamos esto en acción.
\n\nEjemplo 1: Estructura segura en la pila
\npackage main\n\nimport "fmt"\n\ntype Point struct {\n X, Y int\n}\n\n// createPoint crea un Point y lo devuelve por valor.\nfunc createPoint(x, y int) Point {\n p := Point{X: x, Y: y}\n return p // p se copia al devolverlo\n}\n\nfunc main() {\n p1 := createPoint(10, 20)\n fmt.Println(p1)\n}\n\nEn este ejemplo, p es una variable local. Se crea dentro de createPoint y se devuelve por valor (se copia). La estructura p en sí no puede ser utilizada fuera de la función (su copia se usa en main). El escape analysis lo ve y felizmente coloca p en la pila. El GC descansa.
Ejemplo 2: Escape a través de un puntero
\nAhora cambiemos solo una línea: devolvamos un puntero.
\npackage main\n\nimport "fmt"\n\ntype Point struct {\n X, Y int\n}\n\n// createPointer crea un Point y devuelve un puntero a él.\nfunc createPointer(x, y int) *Point {\n p := P