⏱️ Reading time: 16 min

The Goose language compiles to a single C file and, according to its own benchmarks, runs faster than hand-optimized C++ and the fastest version of safe Rust: no heap, no garbage collector, and not a single lifetime annotation in the code. It was created by Wouter van Oortmerssen, known as aardappel, the same developer behind Lobster and co-author of FlatBuffers at Google.

📑 En este artículo
  1. TL;DR
  2. What Is the Goose Language?
  3. Why It Matters
  4. How It Works in Detail
  5. Practical Examples
  6. Getting Started
  7. Real-World Use Cases
  8. Common Mistakes and Best Practices
  9. Comparison With Alternatives
  10. Going Deeper
  11. Frequently Asked Questions
    1. Is the Goose Language Ready for Production?
    2. Does Goose Need a Background Garbage Collector?
    3. How Does Goose Avoid Memory Errors Without Annotations Like Rust’s?
    4. Does Goose Replace Rust or C++?
    5. What Does It Mean That Goose Compiles to C?
    6. What About Threads in Goose?
  12. References

The core promise is easy to state and much harder to deliver: C-level speed with safe dynamic memory and no extra syntax to achieve it. Here’s how it pulls this off, what guarantees it offers, and how it differs from C++ and Rust.

TL;DR

  • Goose compiles to C and eliminates the heap, garbage collector, and lifetime annotations without losing memory safety.
  • Structures grow with a simple pointer advance over a data stack, without allocating dynamic memory.
  • The compiler tracks the root of every reference and rejects accesses that could outlive their owner.
  • Goose enums take up the size of their own variant, not the size of the largest one as in a Rust enum.
  • Each Goose thread runs as an isolated program and shares data by copying, with no locks or race conditions.

What Is the Goose Language?

Goose is a systems programming language, with syntax close to C and Rust, that eliminates the heap, the garbage collector, and lifetime annotations. Every dynamic value lives on a data stack managed by the compiler, and the Goose language guarantees memory safety without that extra syntax.

The language was born as an evolution of ideas from Lobster, an earlier project by the same author that already proposed managing memory with stacks instead of a traditional heap. Goose takes that idea to the extreme: instead of counting references or tracking pointers with a garbage collector, the compiler proves at compile time that no reference can outlive the variable that holds it.

💭 Key point: FlatBuffers, the binary serialization library Google uses in Android and several of its game engines, came from the same author as Goose. The idea that an in-memory structure can also be its own file format isn’t new in his work: Goose takes it to the entire language.

The syntax is deliberately familiar. Anyone who knows C or Rust can read a Goose program without learning a new paradigm: there are structs, functions, for loops, arithmetic operators, and arrays. The difference lies underneath, in how every byte gets allocated and freed.

References in Goose stay valid even after the array grows a million times. Foto de Mike B en Unsplash

Why It Matters

Systems languages have spent three decades arguing over the same tension: C and C++ give full control over memory but allow use-after-free, buffer overflows, and other bugs that account for most critical software vulnerabilities. Rust solved that problem with the borrow checker, but in exchange it required learning ownership, explicit lifetimes, and, in complex cases, accepting numeric indices instead of direct references because a Vec<T> can reallocate at any time.

Goose attacks the same tension from a different angle. Instead of tracking ownership with annotations, the compiler automatically infers the root of every reference (the variable that bounds its lifetime) and rejects the program if it detects an access that could escape that root. The developer doesn’t write a single lifetime annotation, and the compiler can still give a precise error: it names the reference and its target whenever something doesn’t add up.

The other point that matters is performance. Across sixteen benchmarks, the project reports that Goose runs at 3.3 times the speed of idiomatic C++, 1.16 times the speed of hand-optimized C++, and 1.12 times the speed of the best equivalent safe Rust, while using 1.9, 1.3, and 1.2 times less memory respectively, according to the benchmarks published by the project. These numbers come from the Goose team itself, not an independent third party, but the methodology (sixteen comparable programs, same inputs) is published alongside the source code for each test.

How It Works in Detail

A Goose program has native stack memory (the regular call stack), static data, and N data stacks, where N is calculated by the compiler based on how many execution flows the program needs. Each data stack is a large reservation of address space with a bump pointer: there is no other dynamic memory in the system.

The key rule is that, at any given moment, there is at most one variable-size value active per stack, and that value is always on top. This means growing never moves anything or checks a capacity the way std::vector::push_back does in C++: the compiler already proved at compile time that there’s room, so growing is just advancing the pointer.

flowchart TD
    A["Reserved data stack"] --> B["Bump pointer"]
    B --> C["Dynamic value on top"]
    C --> D["End of scope"]
    D --> E["A single store frees everything"]

Freeing memory is just as direct. When the scope where a stack lives ends, the compiler emits a single instruction: return the bump pointer to where it was before entering the scope. It doesn’t matter whether the structure has a thousand elements or ten million: freeing a giant array costs the same as freeing a single integer, because there’s nothing to traverse and no cascading destructors to invoke.

The other piece of the model is relative references. Instead of an absolute 8-byte memory pointer, a reference inside a Goose structure can be stored as a 1, 2, or 4-byte offset relative to its container. The benefit isn’t just saved space: a structure built this way is position-independent in memory, which means saving it to disk is a simple write, and loading it is a read plus a verification pass that rejects tampered bytes. The in-memory format is, literally, the file format.

Practical Examples

The best way to see the model in action is with the project’s own published example code. We’ll start with the simplest case and work up to the one that demonstrates why references don’t break when the array grows.

A minimal Goose program prints text with print, which accepts several comma-separated arguments and concatenates them:

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

The output is literally the string that was passed: Hello, Goose. There’s no heap involved even in this trivial example: the string lives in the program’s static stack.

The second example, taken directly from the official repository, shows an array that grows a hundred thousand times inside a loop and only gets freed when the block closes:

for round in 3 {
  var scratch: u8[>..] = []; // grows with just a pointer advance
  for i in 100000 { scratch.push((i % 256) as! u8); }
  print("round ", round, ": ", scratch.len, " bytes");
} // the free: a single store to the top of the stack

Each round declares scratch from scratch, fills it with a hundred thousand bytes, and prints it. The expected literal output is:

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

That number doesn’t change between rounds because each round redeclares scratch empty: there’s no accumulation between iterations, and freeing the hundred thousand bytes at the end of each round costs the same instruction as freeing a single byte.

The third example is the one that truly sets Goose apart from a Rust Vec<T> or a C++ std::vector: a reference to the first element of an array stays valid after pushing a million more elements onto it.

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; // still valid, a million pushes later
print(first.weight);
sequenceDiagram
    participant P as Program
    participant S as Data stack
    P->>S: items.push(Item)
    S-->>P: reference to the new element
    P->>S: push 1000000 more elements
    Note over P,S: the initial reference is still valid

push returns a reference to the newly inserted element, and that reference (first) keeps pointing to the same element even though the array has grown a million times. The last line prints 99.5, the value that was assigned to it at the end. In C++ that same operation would require reserving capacity up front with reserve(); in safe Rust, the usual approach is to store a u32 index instead of a direct reference, precisely because a Vec can relocate in memory when it grows.

Getting Started

Goose doesn’t yet publish stable release binaries, so the most direct way to try it is to clone the official repository and follow its step-by-step tutorial. Before that, you need a C compiler installed (gcc or clang on Linux/macOS, or the one bundled with MSVC on Windows), because Goose translates every program into a single .c file before running it; the project itself also includes a backend with embedded TinyCC that compiles and runs a program in the same process, with no separate build step.

Linux and macOS:

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

Windows (PowerShell):

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

From there, the official tutorial walks through how to build the Goose compiler itself and run the first programs with the TinyCC backend, without needing to install an additional build toolchain for the examples. We won’t repeat the exact CLI commands here because the project can change them between commits as long as there’s no tagged release; the source of truth is always the repository.

Every Goose program is translated into a single C source file before it runs. Foto de Meizhi Lang en Unsplash

Real-World Use Cases

As of today, Goose is a research project and experimental compiler, not a codebase with a production track record. The memory model performs best in programs with many short-lived, predictable dynamic structures: parsers that build a tree and discard it entirely at the end, simulators that recreate their state at every step, or game engines where each frame allocates and frees the same data shapes.

⚠️ Heads up: Without release binaries or an announced 1.0 version, using Goose today means accepting that the syntax and CLI can change between commits; it’s not a reasonable bet yet for a project with a delivery deadline.

The relative-reference case also brings it close to a very concrete use: structures that are, at the same time, their own file format. A video game scene tree or a binary dataset built with relative offsets can be written to disk with a single write and loaded back with a read plus a verification step, without an intermediate serialization pass like the one JSON or protobuf need.

Goose’s threading model reinforces that same philosophy: each worker compiles as a separate program with its own memory, and plain values cross typed queues with a memcpy. There are no locks, atomics, or memory ordering to reason about because they simply don’t exist in the language: two Goose threads never share a memory address.

Common Mistakes and Best Practices

The most common mistake when shifting your mindset from C++ or Rust is assuming Goose has some unsafe-style escape hatch. It doesn’t. If the compiler determines that a reference could outlive the variable that holds it, the program simply doesn’t compile, and there’s no way to force it with an annotation.

A typical pattern that triggers this rejection is trying to make a reference declared in an outer scope point to data created inside a nested block that hasn’t closed yet. The official documentation’s example uses a longest function that returns the longer of two slices (u8[:]) and then tries to assign the result, when one of the two slices lives in an inner block, to a variable in the outer scope. The compiler rejects that assignment and the error names both the reference’s root and the target that would violate it, so the message points exactly to which variable needs to move or which scope needs to change.

Another gotcha, less about compilation errors than about expectations: a data stack reserves a large range of address space up front. That’s free on 64-bit systems with virtual memory, but it’s the reason Goose targets systems with that memory model and not, yet, 16-bit addressing microcontrollers.

The best practice that follows from all this is to design data structures with an eye toward which stack will hold them and for how long, the same way one thinks in C about which arena or buffer a piece of data will use, but without having to write that decision as code: the compiler works it out from where you declare each variable.

Comparison With Alternatives

Unlike C++ and Rust, the Goose language doesn’t force a choice between speed and safety, but it isn’t free either: it trades explicit annotations for stricter constraints on how a program can be structured.

OptionWhen to Use ItAdvantageLimitation
C++ (manual)Full control over layout and the team already has memory-safety disciplineMaximum control over every byteNo compiler guarantees: use-after-free and overflows possible
Rust (safe)Mature ecosystem and tolerance for the cost of explicit ownershipMemory safety proven in large-scale productionRequires annotated lifetimes and sometimes u32 indices instead of references
GoosePrograms with many short-lived inline structures and a focus on performanceZero lifetime annotations, stable references after growthNew language, no mature ecosystem or libraries yet

The Rust row deserves a caveat: Rust’s borrow checker is more expressive than Goose’s root inferrer in cases with complex aliasing (several conditional mutable references), precisely because Rust does allow annotating those cases by hand. Goose bets that most real programs don’t need that extra expressiveness, and in exchange it doesn’t ask you to write a single annotation.

flowchart LR
    subgraph ClasicoCPP["C++ / Rust"]
    A1["Heap"] --> A2["Allocator"]
    A2 --> A3["Possible reallocation"]
    end
    subgraph GooseLang["Goose"]
    B1["Data stack"] --> B2["Bump pointer"]
    B2 --> B3["Never moves"]
    end

Going Deeper

Relative references aren’t just a space optimization. By storing a 1, 2, or 4-byte offset instead of an absolute pointer, the entire structure becomes position independent: it doesn’t matter what memory address it ends up loaded at. That’s what allows an in-memory structure to be treated as its own file format, something neither C++ nor Rust solves natively, where the normal approach is to write a separate serializer, like protobuf or FlatBuffers.

Goose enums (variable-mode ADTs, in the project’s terminology) give each value the size of its own variant instead of the size of the largest variant, which is how a Rust enum works. On the benchmark that specifically exercises this, the project reports 4 times less memory and 2 times the speed compared to the equivalent Rust enum.

Goose generics have no runtime cost: a parameter with no declared type is generic, and higher-order functions are compile-time entities. That’s why xs.filter() { it > 0 } compiles to exactly the loop it looks like in the source code, with no boxing, no vtable, and building the result directly in its final destination.

Another uncommon piece is returning across multiple calls: return err from load returns from a function any number of levels up, verified at compile time, with no exception unwinder, no Result type, and no ? operator at every intermediate call. It’s, in essence, the same outcome Rust’s error propagation achieves, but resolved in static checking instead of with a container type that has to be manually unwrapped at every step.

Since every Goose program ends up as a single C file, there’s a concrete way to confirm that the memory model works as the project describes: generate that intermediate .c file and search it for calls to malloc, calloc, or free with a simple grep. If the program doesn’t use extern fn to call a C library that does allocate dynamic memory, those calls shouldn’t appear. It’s not a formal guarantee, it doesn’t replace the compiler’s own type proof, but it’s the most direct way to verify it from the outside without extra instrumentation.

Your next step: clone the Goose repository, open the items.push example with the million insertions, change the type of first to a numeric index the way Rust would, and compare how many lines it takes to achieve what a single reference does.

📬 Get new articles by email

We only email about big articles (1-2 a month).

Frequently Asked Questions

Is the Goose Language Ready for Production?

Not according to the project itself: there are no stable release binaries or an announced 1.0 version, so the syntax and CLI can change between commits in the repository.

Does Goose Need a Background Garbage Collector?

No. Dynamic memory lives entirely in data stacks with a bump pointer; freeing a scope is a single instruction, with no collection cycle or pauses.

How Does Goose Avoid Memory Errors Without Annotations Like Rust’s?

The compiler infers the root, the owning variable, of every reference and rejects any access that could outlive that root, without the programmer writing lifetimes by hand.

Does Goose Replace Rust or C++?

Not yet for projects with a production track record: Rust and C++ have mature ecosystems, libraries, and tooling that Goose, as a new project, doesn’t have yet.

What Does It Mean That Goose Compiles to C?

That every Goose program is translated into a single C source file before running, which lets it run on any platform with a C compiler and call C code directly with extern fn.

What About Threads in Goose?

Each thread compiles as a separate program with its own memory; data that crosses between threads does so by copying (memcpy) through typed queues, so there are no locks, atomics, or race conditions in the language.

References

  • aardappel.github.io/goose: the project’s official site, with the tutorial, language reference, and the benchmarks cited in this article.
  • github.com/aardappel/goose: the source code repository and the starting point for cloning and trying out the compiler.
  • doc.rust-lang.org: the ownership chapter of the official Rust book, used as a reference for the memory model comparison.
  • en.wikipedia.org: a general definition of memory safety and the classes of bugs these models aim to prevent.

📱 Enjoy this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.

Featured image: Foto de Abdul Kayum en Unsplash

Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.

Leave a comment
Categories: Tech NewsTutorials

Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

You can include code inside <code>…</code> or, for several lines, <pre><code>…</code></pre>.

This site uses Akismet to reduce spam. Learn how your comment data is processed.