⏱️ Lectura: 18 min

El lenguaje Goose compila a un único archivo en C y, según sus propios benchmarks, corre más rápido que C++ optimizado a mano y que la versión más rápida de Rust seguro: sin heap, sin recolector de basura y sin una sola anotación de vida útil en el código. Lo creó Wouter van Oortmerssen, conocido como aardappel, el mismo desarrollador de Lobster y coautor de FlatBuffers en Google.

📑 En este artículo
  1. TL;DR
  2. ¿Qué es el lenguaje Goose?
  3. Por qué importa
  4. Cómo funciona en detalle
  5. Ejemplos prácticos
  6. Cómo empezar
  7. Casos de uso reales
  8. Errores comunes y buenas prácticas
  9. Comparativa con alternativas
  10. Profundizando
  11. Preguntas frecuentes
    1. ¿El lenguaje Goose ya es apto para producción?
    2. ¿Goose necesita un recolector de basura en segundo plano?
    3. ¿Cómo evita Goose los errores de memoria sin anotaciones como las de Rust?
    4. ¿Goose reemplaza a Rust o a C++?
    5. ¿Qué significa que Goose compile a C?
    6. ¿Qué pasa con los hilos en Goose?
  12. Referencias
    1. 📚 Artículos relacionados

La promesa central es simple de enunciar y mucho más difícil de lograr: velocidad de C con memoria dinámica segura y sin sintaxis adicional para conseguirlo. Acá va cómo lo logra, qué garantías ofrece y en qué se diferencia de C++ y de Rust.

TL;DR

  • Goose compila a C y elimina heap, recolector de basura y anotaciones de vida útil sin perder seguridad de memoria.
  • Las estructuras crecen con un simple avance de puntero sobre una pila de datos, sin reservar memoria dinámica.
  • El compilador rastrea la raíz de cada referencia y rechaza accesos que podrían sobrevivir a su dueño.
  • Los enums de Goose ocupan el tamaño de su propia variante, no el de la más grande como en un enum de Rust.
  • Cada hilo de Goose corre como programa aislado y comparte datos por copia, sin locks ni condiciones de carrera.

¿Qué es el lenguaje Goose?

Goose es un lenguaje de programación de sistemas, con sintaxis cercana a C y Rust, que elimina el heap, el recolector de basura y las anotaciones de vida útil. Cada valor dinámico vive en una pila de datos que gestiona el compilador, y el lenguaje Goose asegura memoria segura sin esa sintaxis adicional.

El lenguaje nació como evolución de las ideas de Lobster, un proyecto anterior del mismo autor que ya proponía manejar la memoria con pilas en lugar de un heap tradicional. Goose lleva esa idea al extremo: en vez de contar referencias o rastrear punteros con un recolector de basura, el compilador prueba en tiempo de compilación que ninguna referencia puede sobrevivir a la variable que la contiene.

💭 Clave: FlatBuffers, la librería de serialización binaria que Google usa en Android y en varios de sus motores de juego, nació del mismo autor que Goose. La idea de que una estructura en memoria sea también su formato de archivo no es nueva en su trabajo, Goose la lleva al lenguaje entero.

La sintaxis es deliberadamente familiar. Quien conozca C o Rust puede leer un programa en Goose sin aprender un paradigma nuevo: hay structs, funciones, bucles for, operadores aritméticos y arreglos. La diferencia está debajo, en cómo se reserva y libera cada byte.

Las referencias en Goose siguen válidas incluso después de que el arreglo crezca un millón de veces. Foto de May en Unsplash

Por qué importa

Los lenguajes de sistemas llevan tres décadas discutiendo la misma tensión: C y C++ dan control total sobre la memoria pero permiten use-after-free, buffer overflows y otros bugs que generan la mayoría de las vulnerabilidades críticas de software. Rust resolvió ese problema con el borrow checker, pero a cambio pidió aprender ownership, lifetimes explícitos y, en casos complejos, aceptar índices numéricos en lugar de referencias directas porque un Vec<T> puede reallocar en cualquier momento.

Goose ataca la misma tensión desde otro ángulo. En lugar de rastrear ownership con anotaciones, el compilador infiere automáticamente la raíz de cada referencia (la variable que delimita su vida útil) y rechaza el programa si detecta un acceso que podría salir de esa raíz. El desarrollador no escribe una sola anotación de vida útil, y aun así el compilador puede dar un error preciso: nombra la referencia y su destino cuando algo no cuadra.

El otro punto que importa es el rendimiento. Sobre dieciséis benchmarks, el proyecto reporta que Goose corre a 3,3 veces la velocidad de C++ idiomático, 1,16 veces la de C++ optimizado a mano y 1,12 veces la del mejor Rust seguro equivalente, usando 1,9, 1,3 y 1,2 veces menos memoria respectivamente, según los benchmarks publicados por el proyecto. Son números del propio equipo de Goose, no de un tercero independiente, pero la metodología (dieciséis programas comparables, mismas entradas) está publicada junto con el código fuente de cada prueba.

Cómo funciona en detalle

Un programa en Goose tiene memoria nativa de pila (la del call stack normal), datos estáticos y N pilas de datos, donde N lo calcula el compilador según cuántos flujos de ejecución necesita el programa. Cada pila de datos es una reserva grande de espacio de direcciones con un puntero de avance (bump pointer): no hay otra memoria dinámica en el sistema.

La regla clave es que, en cualquier momento, hay como mucho un valor de tamaño variable activo por pila, y ese valor siempre está en la cima. Esto significa que crecer nunca mueve nada ni revisa una capacidad como hace std::vector::push_back en C++: el compilador ya probó en tiempo de compilación que hay espacio, así que crecer es solo avanzar el puntero.

flowchart TD
    A["Pila de datos reservada"] --> B["Puntero de avance (bump pointer)"]
    B --> C["Valor dinamico en la cima"]
    C --> D["Fin del scope"]
    D --> E["Un solo store libera todo"]

Liberar memoria es igual de directo. Cuando el scope donde vive una pila termina, el compilador emite una sola instrucción: devolver el puntero de avance a donde estaba antes de entrar al scope. No importa si la estructura tiene mil elementos o diez millones: liberar un arreglo gigante cuesta lo mismo que liberar un entero, porque no hay que recorrer nada ni invocar destructores en cascada.

La otra pieza del modelo son las referencias relativas. En lugar de un puntero de memoria absoluto de 8 bytes, una referencia dentro de una estructura de Goose puede guardarse como un offset de 1, 2 o 4 bytes relativo a su contenedor. El beneficio no es solo ahorro de espacio: una estructura construida así es independiente de la posición en memoria, lo que significa que guardarla en disco es un simple write, y cargarla es un read más una pasada de verificación que rechaza bytes manipulados. El formato en memoria es, literalmente, el formato de archivo.

Ejemplos prácticos

La mejor forma de ver el modelo en acción es con el propio código de ejemplo que publica el proyecto. Partimos de lo más simple y escalamos hasta el caso que demuestra por qué las referencias no se rompen cuando el arreglo crece.

Un programa mínimo en Goose imprime texto con print, que acepta varios argumentos separados por coma y los concatena:

fn main() {
  print("Hola, Goose");
}

La salida es literalmente la cadena que se pasó: Hola, Goose. No hay heap involucrado ni siquiera en este ejemplo trivial: la cadena vive en la pila estática del programa.

El segundo ejemplo, tomado directo del repositorio oficial, muestra un arreglo que crece cien mil veces dentro de un bucle y se libera solo al cerrar el bloque:

for round in 3 {
  var scratch: u8[>..] = []; // crece solo con un avance de puntero
  for i in 100000 { scratch.push((i % 256) as! u8); }
  print("round ", round, ": ", scratch.len, " bytes");
} // el free: un solo store al tope de la pila

Cada ronda declara scratch desde cero, lo llena con cien mil bytes y lo imprime. La salida esperada, literal, es:

round 0: 100000 bytes
round 1: 100000 bytes
round 2: 100000 bytes

Ese número no cambia entre rondas porque cada ronda vuelve a declarar scratch vacío: no hay acumulación entre iteraciones, y liberar los cien mil bytes al cerrar cada ronda cuesta la misma instrucción que liberar un byte.

El tercer ejemplo es el que de verdad separa a Goose de un Vec<T> de Rust o un std::vector de C++: una referencia al primer elemento de un arreglo sigue siendo válida después de empujarle un millón de elementos más.

struct Item { id: i32, weight: f32 }

var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 });
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5; // sigue siendo valida, un millon de pushes despues
print(first.weight);
sequenceDiagram
    participant P as Programa
    participant S as Pila de datos
    P->>S: items.push(Item)
    S-->>P: referencia al nuevo elemento
    P->>S: push 1000000 elementos mas
    Note over P,S: la referencia inicial sigue valida

push devuelve una referencia al elemento recién insertado, y esa referencia (first) sigue apuntando al mismo elemento aunque el arreglo haya crecido un millón de veces. La última línea imprime 99.5, el valor que se le asignó al final. En C++ esa misma operación requeriría reservar capacidad por adelantado con reserve(); en Rust seguro, lo habitual es guardar un índice u32 en vez de una referencia directa, precisamente porque un Vec puede reubicarse en memoria al crecer.

Cómo empezar

Goose todavía no publica binarios de release estables, así que la forma más directa de probarlo es clonar el repositorio oficial y seguir el tutorial paso a paso que lo acompaña. Antes de eso, necesitás un compilador de C instalado (gcc o clang en Linux/macOS, o el que trae MSVC en Windows), porque Goose traduce cada programa a un único archivo .c antes de ejecutarlo; el propio proyecto incluye además un backend con TinyCC embebido que compila y corre un programa en el mismo proceso, sin paso de build aparte.

Linux y macOS:

git clone https://github.com/aardappel/goose.git
cd goose

Windows (PowerShell):

git clone https://github.com/aardappel/goose.git
cd goose

Desde ahí, el tutorial oficial indica paso a paso cómo compilar el propio compilador de Goose y ejecutar los primeros programas con el backend de TinyCC, sin necesidad de instalar un toolchain de build adicional para los ejemplos. No repetimos acá los comandos exactos del CLI porque el proyecto los puede cambiar entre commits mientras no exista una release etiquetada; la fuente de verdad es siempre el repositorio.

Cada programa de Goose se traduce a un único archivo en lenguaje C antes de ejecutarse. Foto de Sandro Moser en Unsplash

Casos de uso reales

Goose es, a fecha de hoy, un proyecto de investigación y compilador experimental, no una base de código con historial de producción. Donde el modelo de memoria rinde mejor es en programas con muchas estructuras dinámicas de vida corta y predecible: parsers que construyen un árbol y lo descartan entero al final, simuladores que recrean su estado en cada paso, o motores de juego donde cada frame reserva y libera las mismas formas de datos.

⚠️ Ojo: Sin binarios de release ni versión 1.0 anunciada, usar Goose hoy significa aceptar que la sintaxis y el CLI pueden cambiar entre commits; no es una apuesta razonable todavía para un proyecto con fecha de entrega.

El caso de las referencias relativas también lo acerca a un uso muy concreto: estructuras que son, a la vez, su propio formato de archivo. Un árbol de escena de un videojuego o un dataset binario construido con offsets relativos puede escribirse a disco con un solo write y volver a cargarse con un read más una verificación, sin un paso de serialización intermedio como el que necesitan JSON o protobuf.

El modelo de hilos de Goose refuerza esa misma filosofía: cada worker se compila como un programa separado con su propia memoria, y los valores planos cruzan colas tipadas con un memcpy. No hay locks, atómicos ni memory ordering que razonar porque, directamente, no existen en el lenguaje: dos hilos de Goose nunca comparten una dirección de memoria.

Errores comunes y buenas prácticas

El error más común al migrar la cabeza desde C++ o Rust es asumir que Goose tiene algún escape hatch tipo unsafe. No lo tiene. Si el compilador determina que una referencia podría sobrevivir a la variable que la contiene, el programa simplemente no compila, y no hay forma de forzarlo con una anotación.

Un patrón típico que dispara ese rechazo es intentar que una referencia declarada en un scope externo apunte a datos creados dentro de un bloque anidado que todavía no cerró. El ejemplo de la documentación oficial usa una función longest que devuelve la más larga de dos franjas (u8[:]) y luego intenta asignar el resultado, cuando una de las dos franjas vive en un bloque interior, a una variable del scope exterior. El compilador rechaza esa asignación y en el error nombra tanto la raíz de la referencia como el destino que la violaría, así que el mensaje señala exactamente qué variable hay que mover o qué scope hay que cambiar.

Otro gotcha, menos sobre errores de compilación que sobre expectativas: una pila de datos reserva un rango grande de espacio de direcciones por adelantado. Eso es gratis en sistemas de 64 bits con memoria virtual, pero es la razón por la que Goose apunta a sistemas con ese modelo de memoria y no, todavía, a microcontroladores con direccionamiento de 16 bits.

La buena práctica que se deriva de todo esto es diseñar las estructuras de datos pensando en qué pila las va a contener y por cuánto tiempo, igual que en C uno piensa en qué arena o qué buffer va a usar un dato, pero sin tener que escribir esa decisión como código: el compilador la calcula a partir de dónde declarás cada variable.

Comparativa con alternativas

A diferencia de C++ y Rust, el lenguaje Goose no exige elegir entre velocidad y seguridad, pero tampoco llega gratis: cambia anotaciones explícitas por restricciones más rígidas sobre cómo se puede estructurar un programa.

Opción Cuándo usarla Ventaja Limitación
C++ (manual) Control total de layout y ya existe disciplina de memory safety en el equipo Máximo control sobre cada byte Sin garantías del compilador: use-after-free y overflows posibles
Rust (seguro) Ecosistema maduro y tolerancia al costo de ownership explícito Seguridad de memoria probada en producción a gran escala Exige lifetimes anotados y a veces índices u32 en vez de referencias
Goose Programas con muchas estructuras inline de vida corta y foco en rendimiento Cero anotaciones de vida útil, referencias estables tras crecer Lenguaje nuevo, sin ecosistema ni librerías maduras todavía

La fila de Rust merece un matiz: el borrow checker de Rust es más expresivo que el inferidor de raíces de Goose en casos con aliasing complejo (varias referencias mutables condicionadas), justamente porque Rust sí permite anotar esos casos a mano. Goose apuesta a que la mayoría de los programas reales no necesitan esa expresividad extra, y a cambio no pide escribir ninguna anotación.

flowchart LR
    subgraph ClasicoCPP["C++ / Rust"]
    A1["Heap"] --> A2["Allocator"]
    A2 --> A3["Posible reallocation"]
    end
    subgraph GooseLang["Goose"]
    B1["Pila de datos"] --> B2["Bump pointer"]
    B2 --> B3["Nunca se mueve"]
    end

Profundizando

Las referencias relativas no son solo una optimización de espacio. Al guardar un offset de 1, 2 o 4 bytes en vez de un puntero absoluto, toda la estructura queda position independent: no importa en qué dirección de memoria termine montada. Eso es lo que permite tratar una estructura en memoria como su propio formato de archivo, algo que ni C++ ni Rust resuelven de forma nativa, donde lo normal es escribir un serializador aparte, como protobuf o FlatBuffers.

Los enums de Goose (variable-mode ADTs, en la terminología del proyecto) le dan a cada valor el tamaño de su propia variante en lugar del tamaño de la variante más grande, que es como funciona un enum de Rust. El proyecto reporta, sobre el benchmark que ejercita esto específicamente, 4 veces menos memoria y 2 veces la velocidad frente al enum de Rust equivalente.

Los genéricos de Goose no tienen costo en runtime: un parámetro sin tipo declarado es genérico, y las funciones de orden superior son entidades de tiempo de compilación. Por eso xs.filter() { it > 0 } compila exactamente al bucle que parece ser en el código fuente, sin boxing, sin vtable y construyendo el resultado directo en su destino final.

Otra pieza poco común es el retorno a través de múltiples llamadas: return err from load retorna desde una función cualquier número de niveles hacia arriba, verificado en tiempo de compilación, sin un unwinder de excepciones, sin un tipo Result y sin un operador ? en cada llamada intermedia. Es, en esencia, el mismo resultado que logra la propagación de errores de Rust, pero resuelto en el chequeo estático en lugar de con un tipo contenedor que hay que desenvolver manualmente en cada paso.

Como todo programa de Goose termina siendo un único archivo en C, hay una forma concreta de confirmar que el modelo de memoria funciona como describe el proyecto: generar ese archivo .c intermedio y buscar en él llamadas a malloc, calloc o free con un simple grep. Si el programa no usa extern fn hacia una librería de C que sí reserve memoria dinámica, esas llamadas no deberían aparecer. No es una garantía formal, no sustituye la prueba de tipos del propio compilador, pero es la forma más directa de verificarlo desde afuera sin instrumentación adicional.

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: cloná el repositorio de Goose, abrí el ejemplo de items.push con el millón de inserciones, cambiá el tipo de first por un índice numérico como haría Rust y compará cuántas líneas hacen falta para lograr lo mismo que una sola referencia.

📬 Recibí lo nuevo en tu email

Te avisamos de artículos grandes (1-2 por mes).

Preguntas frecuentes

¿El lenguaje Goose ya es apto para producción?

No según el propio proyecto: no hay binarios de release estables ni una versión 1.0 anunciada, así que la sintaxis y el CLI pueden cambiar entre commits del repositorio.

¿Goose necesita un recolector de basura en segundo plano?

No. La memoria dinámica vive entera en pilas de datos con un puntero de avance; liberar un scope es una sola instrucción, sin ciclo de recolección ni pausas.

¿Cómo evita Goose los errores de memoria sin anotaciones como las de Rust?

El compilador infiere la raíz, la variable dueña, de cada referencia y rechaza cualquier acceso que pudiera sobrevivir a esa raíz, sin que el programador escriba lifetimes a mano.

¿Goose reemplaza a Rust o a C++?

Todavía no para proyectos con historial de producción: Rust y C++ tienen ecosistemas, librerías y herramientas maduras que Goose, como proyecto nuevo, no tiene aún.

¿Qué significa que Goose compile a C?

Que cada programa en Goose se traduce a un único archivo fuente en lenguaje C antes de ejecutarse, lo que le permite correr en cualquier plataforma con compilador de C y llamar código C directo con extern fn.

¿Qué pasa con los hilos en Goose?

Cada hilo se compila como un programa separado con su propia memoria; los datos que cruzan entre hilos lo hacen por copia (memcpy) a través de colas tipadas, así que no existen locks, atómicos ni condiciones de carrera en el lenguaje.

Referencias

  • aardappel.github.io/goose: sitio oficial del proyecto, con el tutorial, la referencia del lenguaje y los benchmarks citados en este artículo.
  • github.com/aardappel/goose: repositorio del código fuente y punto de partida para clonar y probar el compilador.
  • doc.rust-lang.org: capítulo de ownership del libro oficial de Rust, usado como referencia para la comparación de modelos de memoria.
  • en.wikipedia.org: definición general de seguridad de memoria y de las clases de bugs que estos modelos buscan evitar.

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de Abdul Kayum en Unsplash

¿Te sirvió? ¿Te dio otro error? Contalo abajo: las preguntas se responden y le sirven al siguiente que llegue.

Dejar un comentario

Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Podés incluir código entre <code>…</code> o, para varias líneas, <pre><code>…</code></pre>.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.