Quick Answer: The Goose programming language is a memory-safe systems language with no heap allocator, no garbage collector, and no lifetime annotations. Its compiler places every value on a small set of data stacks and frees memory when a scope ends. The project's own benchmarks show it running faster than idiomatic C++ and safe Rust, but those numbers come from the authors and need checking against your workload.
Most memory-safety stories ask you to pay something. Garbage collectors add pauses, reference counting adds overhead on every copy, and Rust asks you to write lifetime annotations when its checker can't infer them. The Goose programming language claims a different deal: no heap, no collector, no annotations, and still memory safe. That's a bold claim, so here's what the project actually does and where it stops.
How Goose Replaces the Heap With Data Stacks
Goose has no general-purpose heap. A program gets the native call stack, static data, and a number of data stacks that the compiler works out ahead of time. A data stack is a large address-space reservation with a bump pointer, and nothing else.
Why would a language give up the heap entirely? Because a bump pointer makes allocation nearly free and makes deallocation a single operation. When a scope ends, the compiler resets the stack top, and everything above it is gone. Freeing a structure with a million elements takes one store, regardless of how deeply it's nested.
The rule that makes this work is simple: at most one resizable value is live per stack, and it always sits on top. Growth only moves the top forward, so nothing ever has to move. Here's a grow-only array from the project's documentation:
for round in 3 {
var scratch : u8 [ >.. ] = [] ;
for i in 100000 { scratch . push ((i % 256) as ! u8) ; }
}
Each round's array grows by pointer bumps. When the round ends, the space is reclaimed by one store. Most guides skip this part, but it's the reason the rest of the design holds together.
Why Stable References Change the Performance Picture
Here's where most comparisons go wrong. In C++, a std::vector may reallocate when it grows, so any pointer into it can dangle. The standard fix is to call reserve up front, which only works if you know the final size. In safe Rust, the usual workaround is to store u32 indices instead of references.
Goose avoids both. A reference into a growing array stays valid for as long as the array does. push returns a reference to the new element, so you can link elements while you build the array. According to the project documentation, this stable-reference behavior is a major source of its benchmark gains.
The headline numbers, which the project reports across sixteen benchmarks, are these: Goose runs at 3.3x the speed of idiomatic C++, 1.16x the speed of hand-optimized C++, and 1.12x the speed of the best safe Rust. It uses 1.9x, 1.3x, and 1.2x less memory against those same baselines. Treat these as the authors' measurements. The repository also says the benchmarks include cases where Goose loses, which is worth checking before you trust any single ratio.
That said, the real lesson isn't the ratio. Stable references remove a whole class of defensive code, such as reserve calls, index arithmetic, and capacity checks.
Memory Safety Without Lifetime Annotations
This is the claim most people will doubt. Rust needs explicit lifetimes in functions like longest, where the compiler can't tell which input a returned reference came from. Goose handles the same function without any annotation. Every reference and slice has a static root, meaning the variable that bounds its target's lifetime. The compiler infers roots and specializes functions to match.
Here's the counter-intuitive part. Safety doesn't have to come from a programmer-visible lifetime language. It can come from inference plus a rule that refuses to store a reference where it could outlive its owner. When the check fails, the error names the root and the destination:
var outer : u8 [ >.. ] = [] ;
var w = outer[ .. ] ;
{
var inner : u8 [ >.. ] = [] ;
w = longest(outer, inner) ; // error: rooted at inner, does not outlive w
}
The language also drops aliasing and exclusivity rules, and the project says there is no unsafe escape hatch. Those are strong guarantees. They're also rules you should verify in the specification before you rely on them in production, since the spec, not this article, is the authority on what the checker accepts.
Inline Layouts and Variable-Size Enums
Goose stores nested data inline. A string, an array of strings, and a record with variable-size fields are each one contiguous block with no internal pointers. The project's order-book example makes this concrete. One order, with a customer name and a list of line items, takes 29 bytes and zero allocations in Goose. The equivalent C++ std::string plus std::vector<Item> takes 160 bytes and one allocation, and the Rust String plus Vec<Item> takes 153 bytes and four allocations.
Enums get the same treatment. Goose has two modes for algebraic data types. Fixed mode gives every value room for the largest variant, so you can index and overwrite values. Variable mode, written with a .. suffix, gives each value only the size of its own variant. The project reports 4x less memory than a Rust enum and 2x its speed, but only on the benchmark that exercises this, so don't generalize it.
The trade-off is real. An array of variable-size elements can be iterated but not indexed, unless you maintain a second array of references as an index. Pick the layout based on access pattern, not on memory alone.
Compiling to C and Threads With No Shared Memory
Goose compiles to a single C file. That means it runs wherever a C compiler does, and it can call C directly through extern fn. The bundled TinyCC backend goes further: it compiles and runs a program in-process, which removes the build step during development.
Concurrency works differently too. Each worker is compiled as a separate program with its own memory. Flat values move between threads through typed queues using a memcpy. Because nothing is shared, data races, locks, atomics, and memory orderings don't appear in the language at all. Here's the trade: you can't share a mutable structure between workers, so you design your pipeline around message passing from the start.
Goose vs C++ and Rust at a Glance
| Aspect | Goose | C++ (std::vector, std::string) | Safe Rust (Vec, String) |
|---|---|---|---|
| Memory model | Data stacks, bump pointer, freed at scope exit | Heap allocation, manual or RAII cleanup | Heap allocation, ownership and drop |
| Order record size (project benchmark) | 29 bytes, 0 allocations | 160 bytes, 1 allocation | 153 bytes, 4 allocations |
| Lifetime annotations | None, inferred by compiler | Not applicable, no borrow checker | Required in some functions, such as longest |
| Reference stability on growth | Stays valid while the array lives | Invalidated by reallocation, use reserve | Invalidated by reallocation, use indices |
Notice what the table can't show: maturity. Goose is young, so its ecosystem, package set, and standard library are much smaller than C++ or Rust. For a team that needs mature libraries today, that matters more than any benchmark.
Where Goose Fits, and Where It Doesn't
Here's where it gets interesting. Goose looks most attractive for workloads that build large, nested, grow-only data and then read it sequentially, such as parsers, build tools, indexing, and data pipelines. Its serialization story is also unusual: relative references store narrow offsets, so a structure can be written to a file as is, and from_bytes verifies the bytes before returning a value. A corrupt file produces false, not a wild pointer.
That said, the costs are specific. Scope-based freeing means you need to think about where long-lived data lives. The project's answer for non-nested lifetimes is a pool that pairs a grow-only array with a hidden freelist, and a stale slot reads a different value of the right type instead of corrupting memory. That's safe, but it's a design you must plan for, not something that happens automatically.
Most people stop here, and that's a mistake. Ask whether your hot path is allocation-bound or compute-bound. If it's compute-bound, Goose's advantages shrink. If it's allocation-bound, the inline layouts and stable references are where the gains come from.
Frequently Asked Questions
What is the Goose programming language?
The Goose programming language is a memory-safe systems language that compiles to a single C file. It has no heap allocator, no garbage collector, and no lifetime annotations. Values live on compiler-managed data stacks and are freed when their scope ends. Its syntax will look familiar to C and Rust programmers.
How do I start learning the Goose programming language?
Begin with the tutorial on the project site, which teaches the language through examples. Then read the reference for specific topics and the specification for exact rules. Because the TinyCC backend compiles and runs programs in-process, you can experiment without a separate build step.
Why does Goose not need a garbage collector?
Goose doesn't need a collector because the compiler decides where every value lives before the program runs. Each reference has a static root, and the rules reject any reference that could outlive its owner. At runtime, only the bump pointers move, so freeing a scope is one store to the stack top.
Does Goose require lifetime annotations like Rust?
No. The compiler infers the root of each reference and specializes functions for those roots. When a lifetime rule fails, the error names the reference's root and its destination, so you can see the problem. The trade-off is that you must trust the checker's rules, which are defined in the language specification.
Goose is a useful experiment in what memory safety can cost when you remove the heap, the collector, and the annotations. Try rewriting one allocation-heavy loop from your own code in the Goose tutorial this week, then compare the memory footprint and the number of reallocation-related workarounds you can delete. Pass this to someone wrestling with allocator overhead in a systems project who wants a safer model without a garbage collector.