📦aardappel / goose
← 返回首页
The Goose Programming Language
⚖️ Apache-2.0📅 创建于 2026-08-30🔄 更新于 2026-11-01

📄 仓库 README

在 GitHub 查看 ↗
来自 GitHub 仓库自述文件 · 版权归原作者所有

The Goose programming language

The Goose Programming Language

A memory-safe systems language with no heap allocator, garbage collector, or lifetime annotations. In this repository's benchmarks, Goose runs faster than C++ and Rust while using less memory.

Tutorial · Reference · Specification · Command line · Packages · Samples · Benchmarks · Standard library

Goose has syntax familiar to C and Rust programmers, but there is no heap. Every dynamic value lives inline on data stacks managed by the compiler. Growth advances a pointer, and memory is freed when its scope ends. The language's type and lifetime rules make this memory model safe to use.

Discord Chat Twitter Follow

Why Goose

  • Faster than C++ and than safe Rust, while memory safe. Over sixteen benchmarks Goose runs at 3.3x the speed of idiomatic C++, 1.16x hand-optimized C++ and 1.12x the best safe Rust, and uses 1.9x, 1.3x and 1.2x less memory (results). These gains come from Goose's data layouts and memory model.
  • No allocator, no GC, no reference counting, no destructors. Memory is a handful of data stacks that the compiler assigns statically. Freeing a structure with a million elements takes a single store, no matter how deeply it is nested.
  • Growth preserves references. A reference into a growing array stays valid for as long as the array does. In the corresponding benchmarks, Goose keeps typed references where C++ must call reserve and safe Rust uses u32 indices.
  • Memory safe with zero annotations. There is no lifetime syntax, there are no aliasing or exclusivity rules, and there is no unsafe. The compiler infers what every reference is rooted at and rejects references that could outlive their owners.
  • Nested data stays inline. A string, an array of strings, a record with variable-size fields and an array of those records are each stored as one contiguous block that contains no pointers. A example record that takes 160 bytes and one allocation in C++ takes 29 bytes and no allocations in Goose.
  • Variable sized Enums. Variable-mode ADTs store each value at the size of its own variant rather than that of the largest variant. On the benchmark that exercises this, they use 4x less memory than a Rust enum and run at 2x its speed.
  • Links narrower than pointers. A relative reference stores a typed, checked link as a 1, 2 or 4-byte offset. Structures built from them are position independent, so a data structure can be written to a file as is. Saving it is a single write, and loading it is a single read followed by a verification pass that rejects malformed or hostile data.
  • Everything is built in place, guaranteed. A value is constructed at its final destination through any depth of calls. items.push(parse(line)) writes the parsed record directly into the array, and returning a growable array by value does not copy it.
  • Returns across multiple calls. return err from load returns from a function any number of frames up the call stack. It is checked statically, and needs no unwinder, no Result type and no ? on every call.
  • Threads with no shared memory. A worker is compiled as a separate program with its own memory, and flat values are passed between threads through typed queues with a memcpy. Data races, locks, atomics and memory orderings do not exist in the language.
  • Generics and higher-order functions with no overhead. An untyped parameter is generic. Function values are compile-time entities, so xs.filter() { it > 0 } compiles to what the handwrittend loop would looks like and builds its result directly in its destination.
  • Compiles to C and can call C. Goose compiles to one C file, so it runs wherever a C compiler does and calls C directly through extern fn. The bundled TinyCC backend compiles and runs a program in-process, with no build step.

The tutorial walks through all of this by example, the reference covers the whole language topic by topic, the specification has the exact rules, and the benchmarks have the numbers, including the cases where Goose loses.

Features

This section focuses on what distinguishes Goose. The tutorial explains these features in more detail, and the samples show them in thirty-one complete programs.

One memory model: data stacks freed at scope exit

A program has the native call stack, static data and N data stacks, where the compiler works out N. A data stack is a large address-space reservation with a bump pointer, and there is no other memory. At most one resizable value is live per stack and it is always on top, so growth never moves anything and never checks a capacity. All of this is proved at compile time; the runtime tracks only the bump pointers.

for round in 3 {
    var scratch: u8[>..] = [];                        // grow-only: growth is a pointer bump
    for i in 100000 { scratch.push((i % 256) as! u8); }
    print("round ", round, ": ", scratch.len, " bytes");
}                                                     // the free: one store to the stack top

Memory never moves, so references survive growth

struct Item { id: i32, weight: f32 }

var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 });             // a reference to element 0
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5;                                  // still valid, a million pushes later

push returns a reference to the new element, so a program can link elements while building the array. A vector<T> or Vec<T> may reallocate and cannot provide this guarantee. Stable references account for several benchmark gains.

Safe references, no annotations

Every reference and slice has a static root: the variable that bounds its target's lifetime. A reference must not outlive the variable that owns its target or access that target through an incompatible type. The compiler infers roots and specializes functions for them, so no annotations are needed. There are no aliasing or exclusivity rules. When a lifetime check fails, the error names the reference's root and its destination:

fn longest(a: u8[:], b: u8[:]) -> u8[:] { if a.len >= b.len { a } else { b } }

var outer: u8[>..] = [];
var w = outer[..];
{
    var inner: u8[>..] = [];
    format(inner, "inner text");
    w = longest(outer, inner);     // error: storing a reference rooted at inner,
}                                  //        which does not outlive the destination (§9.2)

A slice T[:] lets a function accept a range from any array kind without copying its elements. For example, split returns slices into its input, and a dictionary keyed by u8[:] stores views of strings rather than copies.

Nested data stays inline

There is no single array type. Instead there is a family of array types that differ only in how their size can change. Every member is laid out as [metadata][elements...], inline and packed, never as a pointer to a separate block of elements:

Type What it is
T[k] fixed size, known at compile time
T[], T[varint] sized at construction, frozen after; a fixed or variable-width length
T[..k], T[..] capacity inline; grows and shrinks within it
T[>..] grow-only: the most commonly used kind; references into it stay valid
T[>..<] grow-shrink: stacks, queues, heaps

Strings are u8 arrays: u8[>..] is a builder, u8[] a finished string stored inline in whatever holds it, u8[..16] a small string inside a struct, u8[:] a view. A struct may contain variable-size parts, which are stored inline in declaration order, so a record is a sequence of bytes with no indirection in it:

struct Item  { sku: u8[varint], qty: varint, cents: varint }
struct Order { id: varint, customer: u8[varint], items: Item[varint] }

var book: Order[>..] = [];
book.push(Order { id: 1001, customer: "alice",
                  items: [Item { sku: "SKU-441", qty: 2, cents: 1999 },
                          Item { sku: "SKU-7", qty: 1, cents: 500 }] });

That order is 29 bytes and 0 allocations. As C++ std::string + std::vector<Item> it is 160 bytes and 1 allocation, and as Rust String + Vec<Item> it is 153 bytes and 4 allocations. An entire order book is a single array, so reading it streams through the cache. The trade-off is that an array of variable-size elements is sequential: you can iterate it but not index it (unless you keep a second array of references into it as an index).

Enums in two sizes

Algebraic data types are the only form of dynamic polymorphism; there is no inheritance and there are no vtables. Every ADT can be stored in two ways, chosen at the point of use. Fixed mode (Shape) stores a tag plus room for the largest payload, so values can be indexed and overwritten. Variable mode (Shape..) gives each value exactly the size of its variant. An array of such values becomes sequential and a value never changes variant, but in exchange you may take references into a payload:

```goose enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }

var packed: Shape..[>..] = []; // 9, 17 and 1 bytes, not 17 each packed.push(Shape.Circle { r: 1.0 }); packed.push(Shape.Rect { w: 2.0, h: 3.0 }); packed.push(Shape.Dot); for s in packed { mat

📈 近 7 天 Star 趋势

每日新增 Star(柱)与累计 Star(线)· 数据来自 GitHub

🏆 排名变化趋势

在各榜单中的排名历史(数字越小越靠前)

📋 历史上榜记录

每次快照的详细数据
日期 榜单 排名 周期新增 总 STAR