Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

En general la memoria de una computadora se puede clasificar en:

Memoria primaria (o central)
Constituida por memoria volátil, más rápida y costosa que otros medios de almacenamiento. Es la memoria de trabajo del procesador, la RAM (Random Access Memory) de acceso rápido, donde se encuentran los programas en ejecución y sus datos. La información que almacena la RAM se pierde al interrumpirse el suministro eléctrico.
Memoria secundaria
Conformada por el conjunto de memorias no volátiles. Es la memoria de persistencia de información. Ejemplos: discos rígidos, discos ópticos, memorias USB, etc. Conserva la información almacenada al interrumpirse el suministro de corriente eléctrica. Es más lenta y barata que la RAM.

1Organización de la memoria primaria

Tanto los programas como el resto de la información que guarda la computadora se encuentran almacenados en la memoria secundaria.

Al lanzar la ejecución de un programa, las instrucciones y los datos iniciales se copian a la memoria primaria. Un programa en ejecución se denomina proceso.

En tiempo de ejecución, un proceso está asociado a una porción de la memoria primaria que se divide lógicamente en 4 segmentos:

Segmento de código (code segment)
Es la porción donde se localizan las instrucciones que componen nuestro programa. Su tamaño se determina al comenzar la ejecución. Asociado a este segmento se encuentra un puntero que indica la próxima instrucción a ejecutar.
Segmento de datos (data segment)
Almacena las variables globales y estáticas. Su tamaño también queda determinado al comenzar la ejecución.
Pila (stack segment)
Almacenará el contenido de las variables locales en cada invocación de una función. Cada entrada en la pila constituye un marco de datos (stack frame) que representa el contexto de una función en ejecución e incluye variables locales, parámetros y valores de retorno. Se asigna como un bloque de memoria contigua.
En Go, el tamaño inicial del stack es pequeño (generalmente 2KB) pero no es de tamaño fijo como en C, sino que tiene tamaño dinámico (el runtime lo agranda si hace falta).
Memoria dinámica (heap)
Es el espacio de memoria que se utiliza para la asignación dinámica de memoria. Su tamaño no está determinado al comenzar la ejecución y se va ajustando a medida que el programa solicita más memoria para almacenar datos. No se asigna como un único bloque contiguo, sino que puede fragmentarse.

2Ejecución de programas

Estos son algunos de los aspectos a tener en cuenta al momento de ejecutar un programa:

1. Compilación y almacenamiento.
Los programas se almacenan inicialmente en la memoria secundaria (por ejemplo, en un disco duro o SSD). En el caso de Go como se trata de un lenguaje compilado, los archivos fuentes se compilan y se genera un archivo ejecutable que se almacena en el disco.
2. Asignación de memoria central
Al iniciar la ejecución, el sistema operativo carga el programa en la memoria central (RAM). Esto incluye las instrucciones del programa y los datos iniciales necesarios para su ejecución. La memoria asignada al programa se divide en segmentos específicos para el código, los datos, el stack y el heap. El programa en ejecución se denomina proceso.
3. Ejecución del programa
El procesador ejecuta una a una las instrucciones del programa desde el segmento de código. Cada vez que ejecuta una instrucción, avanza el puntero de instrucción a la siguiente instrucción. Cuando ejecuta una llamada a una función, se crea un nuevo frame en el stack para almacenar las variables locales y los parámetros de la función. Al terminar la función, el stack frame se elimina y el valor de retorno se transfiere al marco anterior desde donde se llamó a la función. Si durante la ejecución de una función se solicita memoria dinámica, se asigna en el heap.
En Go, durante la ejecución del programa, el recolector de basura (garbage collector) se encarga de liberar la memoria no utilizada en el heap, evitando así las fugas de memoria (memory leaks).
4. Interacción con el sistema operativo
El sistema operativo supervisa y gestiona la memoria asignada al programa. Si el programa necesita más memoria, puede solicitarla al sistema operativo, que ajustará el tamaño del heap o el stack según sea necesario.
5. Liberación de memoria
Al finalizar la ejecución, el sistema operativo libera toda la memoria asignada al programa, incluyendo los segmentos de código, datos, pila y heap.

En la siguiente figura se muestra un esquema de la memoria de un proceso en ejecución. Cada segmento de memoria tiene un tamaño y una función específica en el programa. La figura es solo a modo didáctico y no representa la organización real de la memoria en Go, que es más compleja.

En el diagrama el stack se ubica en la parte superior de la memoria y crece hacia abajo; cuando no puede crecer más se produce un error de desbordamiento de pila (stack overflow). El heap se ubica en la parte inferior de la memoria, sobre los segmentos de código y datos y crece hacia arriba.

Segmentos de memoria de un proceso en ejecución

Segmentos de memoria de un proceso en ejecución

Segmentos de memoria de un proceso en ejecución

Segmentos de memoria de un proceso en ejecución

3Gestión de memoria dinámica en Go

La gestión de memoria es un aspecto clave en cualquier lenguaje de programación, ya que impacta en el rendimiento, la eficiencia y la estabilidad del software.

En Go, las variables se almacenan en el stack o en el heap dependiendo de su alcance, duración y cómo se utilizan.

El compilador de Go decide automáticamente si una variable debe almacenarse en el stack o en el heap. Esto se conoce como Escape Analysis. Si una variable “escapa” del alcance de la función, se almacena en el heap en lugar del stack.

Veamos un ejemplo, dado el siguiente fragmento de código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Direccion struct {
    calle, ciudad, provincia string
    numero                   uint
}

type Persona struct {
    nombre, apellido string
    edad             uint
    direccion        Direccion
}

var num int = 5
var p1 = Persona{"Marcelo", "Díaz", 27, Direccion{"Mariano Acosta",
                  "Gonzalez Catán", "Buenos Aires", 6420}}

func main() {
    p2 := Persona{nombre: "Pepe", edad: 23}
    p3 := &p2
    p4 := Persona{"Juan", "Gonzalez", 34, Direccion{"Valentín Gómez",
                  "Caseros", "Buenos Aires", 742}}

    p3.direccion := Direccion{"Av. Corrientes", "CABA", "Buenos Aires", 1050}
    p3.apellido = "Martinez"
    p4.direccion = Direccion{"Mariano Acosta", "Gonzalez Catán",
                             "Buenos Aires", 6420}
}

El Stack, el Heap y el Segmento de Datos presentarán el siguiente estado:

Mapa de Memoria

Mapa de Memoria

Mapa de Memoria

Mapa de Memoria

Vamos a analizar cada variable en el código y justificar dónde se almacena:


Resumen general:

VariableUbicaciónJustificación
numSegmento de DatosVariable global simple.
p1Segmento de Datos (header) / Heap (str)Estructura global, contenido de cadenas en heap.
p2Stack (estructura) / Heap (cadenas)Variable local, contenido de cadenas en heap.
p3StackPuntero local a p2.
p4Stack (estructura) / Heap (cadenas)Variable local, contenido de cadenas en heap.

En Go, el compilador y el recolector de basura (GC) optimizan el uso del stack y el heap. Las estructuras simples y de corta duración suelen estar en el stack, mientras que los datos más complejos o de mayor duración (como cadenas) se almacenan en el heap.

Go utiliza un recolector de basura concurrente para liberar memoria automáticamente. Es concurrente porque la mayor parte del trabajo de limpieza ocurre en paralelo con la ejecución del programa. Esto permite que el programador no tenga que liberar la memoria manualmente, evitando errores comunes como el acceso a memoria inválida.

Cuando existen datos en la memoria dinámica que ya no son accesibles (no están referenciados desde la pila ni el segmento de datos), el GC los marca para su eliminación y eventual liberación.

Para coordinar ciertos pasos del proceso de limpieza, el GC realiza pausas extremadamente breves denominadas stop-the-world (generalmente menores a un milisegundo). Go se destaca por optimizar estas pausas para que no interfieran con el rendimiento percibido del sistema:

4Ejercicios

  1. Mapa de memoria. Dado el siguiente código, dibujá un esquema de la memoria indicando en qué segmento (datos, stack o heap) se almacena cada variable y su contenido:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var global int = 42
    
    type Punto struct {
        X, Y int
    }
    
    func main() {
        a := 10
        b := &a
        c := Punto{X: 3, Y: 7}
        d := "hola"
    }
  2. Escape analysis. Para cada una de las siguientes funciones, indicá si la variable creada escapa al heap o se queda en el stack. Justificá:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func f1() int {
        x := 5
        return x
    }
    
    func f2() *int {
        x := 5
        return &x
    }
    
    func f3() {
        s := make([]int, 10)
        s[0] = 1
        fmt.Println(s[0])
    }
  3. Strings en heap. Explicá por qué el contenido de un string en Go siempre se almacena en el heap, incluso cuando la variable que lo contiene es local. ¿Qué parte de la variable queda en el stack?

  4. Stack frames. Dada la secuencia de llamadas main()calcular()sumar(a, b int), describí el contenido del stack en cada paso. Indicá qué datos contiene cada stack frame (parámetros, variables locales, dirección de retorno).

  5. Garbage collector. Explicá brevemente:

    • Qué problema resuelve el garbage collector concurrente de Go.

    • Qué significa stop-the-world y por qué las pausas son del orden de microsegundos en Go.

    • Qué ventaja tiene un GC concurrente frente a uno no concurrente.