Goose 是一种新兴的系统编程语言,其核心设计理念在于摒弃传统的堆分配器、垃圾收集器(GC)以及复杂的生命周期注释机制。根据官方基准测试数据,Goose 在运行速度上超越了 C++ 和 Rust,同时具备更低的内存占用率。

目前该项目已提供完整的教程、参考文档、语言规范、包管理工具、示例代码、基准测试结果及标准库支持。
对于熟悉 C 或 Rust 语法的开发者而言,Goose 的学习曲线相对平缓。该语言采用编译器管理的数据堆来处理动态值:增长操作仅涉及指针前移,而内存则在变量作用域结束时自动释放。通过严格的类型系统和寿命规则,Goose 确保了这种内存模型的安全性。
核心优势与设计理念
- 性能与安全平衡:在 16 项基准测试中,Goose 的运行速度是惯用 C++ 代码的 3.3 倍,比手动优化的 C++ 快 1.16 倍,比安全的 Rust 快 1.12 倍;内存使用量分别减少至 C++ 的 1/1.9、优化 C++ 的 1/1.3 和 Rust 的 1/1.2。这些性能提升主要得益于其独特的数据布局和内存模型。
- 极简内存管理:无需分配器、GC、引用计数或析构函数。内存由编译器静态分配到一小部分数据堆中。释放包含百万个元素的结构仅需一次存储操作,无论其嵌套深度如何。
- 稳定的引用机制:即使数组发生增长,指向其中元素的引用依然有效。相比之下,C++ 需要保留类型引用,而 Rust 在类似场景下通常需使用 u32 索引替代直接引用。
- 无注释内存安全:无需编写生命周期语法,不存在别名或独占性规则,也不允许使用 unsafe 关键字。编译器会自动推断每个引用的根源,并拒绝任何可能超出所有者生命周期的引用。
- 内联嵌套数据:字符串、字符串数组、含可变大小字段的记录及其数组均以连续块形式存储,内部不包含指针。例如,C++ 中的此类结构需分配 29 字节,而在 Goose 中无需额外分配。
- 精确大小的代数数据类型(ADT):变量模式 ADT 为每个值分配与其实际内容匹配的大小,而非最大变体大小。在相关基准测试中,Rust enum 的内存占用减少了 4 倍,速度提升了 2 倍。
- 窄链接技术:相对引用以 1、2 或 4 字节的偏移量存储经过类型检查的链接。由此构建的结构具有位置独立性,数据结构本身即为文件格式:保存即写入,加载即读取并验证恶意字节。
- 就地构建优化:无论调用深度如何,值均在最终目的地构建。例如
items.push(parse(line))会将解析后的记录直接写入数组,通过值返回可增长数组无任何额外成本。 - 多返回值与错误处理:函数可返回任意数量的负载,经静态检查,无需解卷器、Result 类型或在每次调用后使用
?运算符。 - 线程隔离模型:工作线程被编译为拥有独立内存的单独程序,平面值通过类型队列编译为 memcpy 操作。语言层面不存在数据竞争、锁、原子操作或内存排序概念。
- 零开销泛型:未输入的参数即为通用参数。函数值是编译时实体,因此
xs.filter() { it > 0 }会编译为看似循环的代码,并将结果直接构建到目标位置。 - C 语言互操作性:Goose 编译生成单个 C 文件,可在任何 C 编译器环境中运行,并通过
extern fn直接调用 C 函数。捆绑的 TinyCC 后端支持在进程中编译和运行程序,无需构建步骤。
官方教程通过实例演示上述特性,参考手册涵盖所有语言主题,规范文档则定义了确切规则。
关键特性详解
本节深入剖析 Goose 的核心功能,详细解释见教程,完整实现见 31 个示例程序。
单一内存模型:基于堆与作用域出口的自由释放
程序包含本地调用栈、静态数据和 N 个数据堆,编译器计算这 N 个数据堆。数据堆是一个大型地址空间预留区,仅包含一个凸点指针(bump pointer),无其他内存管理结构。每个堆最多有一个实时可调整大小的值位于顶部,确保增长操作永不移动现有数据,也无需检查容量。所有逻辑均在编译时证明,运行时仅跟踪碰撞指针。
for round in 3 {
var scratch: u8[>..] = [];
for i in 100000 { scratch.push((i % 256) as! u8); }
print("round ", round, ": ", scratch.len, " bytes");
}
引用稳定性:无移动操作保障
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; // 仍然有效,即便在一百万次 push 之后
此机制允许程序在构建数组时链接元素。std::vector<T> 或 Vec<T> 因可能重新分配而无法提供此保证。稳定的引用值是多项基准测试中性能收益的主要来源。
无注释的安全引用
每个引用和切片都有一个静态根,限制其目标的生命周期。引用不能超过拥有目标的变量或通过不兼容类型访问目标。编译器自动推断根并为专用函数生成检查,无需人工注释。若终身检查失败,错误信息将指明引用的根及其目的地:
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)
切片类型 T[:] 允许函数接受任何数组类型的范围而不复制元素。例如,split 返回输入的切片,以 u8[:] 为键的字典存储字符串视图而非副本。
内联嵌套数据结构
Goose 没有单一的数组类型,每个成员均为 [元数据][元素...] 格式,内联且打包,从不指向元素块的指针:
| 拼写方式 | 定义说明 |
|---|---|
T[k] |
编译时已知固定大小 |
T[..] |
构造时确定尺寸,随后不可变;制造时确定尺寸;固定或可变宽度长度 |
T[] |
内联传输能力;在内成长和收缩 |
T[>..] |
仅增长:劳动马;其引用保持有效 |
T[>..<] |
增长和缩小:堆、队列、堆 |
字符串本质上是 u8 数组:u8[>..] 是构建器,u8[] 是内联存储的已完成字符串,u8[..16] 是结构内部的小字符串,u8[:] 是视图。结构可包含按声明顺序排列的可变大小部分,使得记录成为无间接内容的字节流:
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 }] });
此订单序列占用 29 字节且零配置。对比之下,C++ 的 std::string + std::vector<Item> 需 160 字节和 1 次分配,Rust 的 String + Vec<Item> 需 153 字节和 4 次分配。整个订单簿作为缓存友好的连续数组流动。代价是可变大小元素数组只能迭代而不能索引(除非维护单独的引用数组作为索引)。
双模态枚举(Enum)
代数数据类型(ADT)是唯一的多态形式:无继承,无虚表。每个 ADT 有两种存储模式,在使用点选择。固定模式(形状)包含标签和最大有效载荷空间,支持索引和重写。可变模式(形状)为每个值准确分配其变体大小;值不会改变变体,但可将引用转化为有效载荷:
enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, Dot }
var packed: Shape..[>..] = []; // 分别为 9, 17 和 1 字节,而非统一 17 字节
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 {
match s { Rect &r => { r.w += 1.0; }, _ => {} } // 在数组内就地编辑有效载荷
}
案例函数提供另一种调度方式:每种变体定义一个重载。通过跳转表调用标签发送,编译器检查重载是否覆盖所有变体:
fn area(s: Shape.Circle) -> f64 { 3.14159 * s.r * s.r }
fn area(s: Shape.Rect) -> f64 { s.w * s.h }
fn area(s: Shape.Dot) -> f64 { 0.0 }
for s in packed { print(s, " area ", area(s)); }
比指针更窄的链接
T& 是机器地址(8 字节),永不悬空。相对引用以窄偏移量存储相同链接:T&<u32> 从字段自身测量到同一数组中的目标,使结构位置独立;T&<u32 在池> 从命名池基础测量,允许其他数组链接到池中。偏移量 0 表示 null。以下是在仅增长数组中实现的二叉搜索树,节点大小为 12 字节:
struct Node { key: i32, left: Node&<u32>?, right: Node&<u32>? } // 12 字节,含链接
fn insert(pool: Node[>..]&, key: i32) {
if pool.len == 0 { pool.push(Node { key: key }); return; }
var cur .= pool[0]; // .= 绑定引用;= 会复制节点
loop {
if key == cur.key { return; }
let next = if key < cur.key { cur.left } else { cur.right };
if next { cur .= next; continue; }
if key < cur.key { cur.left .= pool.push(Node { key: key }); }
else { cur.right .= pool.push(Node { key: key }); }
return;
}
}
引用透明(无 * 或 ->),.= 用于绑定或重定位,Node? 为无效引用,配合 if、守护语句和断言进行收窄。插入操作仅涉及推进和存储,推进不会使 cur 失效。当生命周期非嵌套时,可复用池将仅增长数组与隐藏的空闲列表配对:alloc_ref 重用插槽或推入新元素,free 归还插槽。由于无实际释放,陈旧插槽读取的是正确类型的不同值,而非破坏内存。
数据即文件格式
var image: u8[>..] = tree.to_bytes(); // 整个数组的框架化字节图像
var loaded, ok = from_bytes<Node[>..]>(image);
image[image.len - 3] = 200; // 篡改链接 ...
var bad, bok = from_bytes<Node[>..]>(image); // ... 结果为 false 和空数组
树的链接是自相对的,无论位置如何含义不变:保存即写字节,加载即读回,无需序列化器、模式或指针修复。from_bytes 在字节转换为值之前检查框架、每个标签、长度和链接。
始终就地构建
无复制构建保证(规范 §4.3)确保构建的值总是在其最终位置建立,通过调用向下传播。函数通过值返回仅增长数组,将其元素直接写入调用者变量或另一数组内的记录字段,因此输出参数大多不会出现。
words.push(str("word", i)); // 格式化直接写入新元素
let evens = xs.filter() { it % 2 == 0 }; // 直接构建到 `evens`:无临时变量
book.push(Order { id: 1001, customer: "alice", items: parse_items("SKU-441:2:1999;SKU-7:1:500") });
调用链中的错误返回
struct User { name: u8[..16], age: i32 }
fn load(text: u8[:]) -> User[>..], u8[] {
var users: User[>..] = [];
each_split(text, '\n') { users.push(parse_user(it)); };
return users, "";
}
fn parse_user(line: u8[:]) -> User { // 返回 User:无 Result,无错误参数
let comma = find(line, ",");
guard comma >= 0 else { return [], str("expected name,age: ", line) from load; }
let age, ok = parse_int(line[comma + 1..]);
guard ok else { return [], str("bad age: ", line) from load; }
let name = trim(line[..comma]);
return User { name: name, age: age as i32 };
}
from f 返回 E 作为 f 的最活跃调用的结果,无论中间有多少层,每个函数保持其平面签名。静态检查确保每个 parse_user 调用必须在 load 调用中,运行时未被捕获的错误为零。这是一个隐藏的分辨器检查每步而非解卷器,消息直接构建到负载调用者所需的位置。示例中的解析器利用此机制解决每个语法错误。
仿制品和功能组件
fn twice(x) { x + x }
fn each_pair<T, F>(xs: T[:]) { // F 是函数值:编译时实体
for i in 0..xs.len - 1 { F(xs[i], xs[i + 1]); }
}
fn first_gap(xs: i64[:])




