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.
reserve and safe Rust uses u32 indices.unsafe. The compiler
infers what every reference is rooted at and rejects references that could
outlive their owners.enum
and run at 2x its speed.items.push(parse(line))
writes the parsed record directly into the array, and returning a growable
array by value does not copy it.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.memcpy. Data races, locks, atomics and memory
orderings do not exist in the language.xs.filter() { it > 0 } compiles to what the handwrittend loop would looks like and builds its
result directly in its destination.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.
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.
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
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.
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.
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).
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
| 日期 | 榜单 | 排名 | 周期新增 | 总 STAR |
|---|