Goose プログラミング言語

ヒープなし・GCなし・ライフタイム注釈なしのメモリ安全なシステム言語

作成日: 2026年10月27日 | 情報源: 公式サイト・GitHub・言語リファレンス

Goose とは

Goose は、Wouter van Oortmerssen(aardappel)によって開発された新しいシステムプログラミング言語です。 C++ や Rust に似た構文を持ちながら、ヒープアロケータなし・ガベージコレクタなし・ライフタイム注釈なしでメモリ安全性を実現します。 公式ベンチマークでは、C++ や安全な Rust を上回る速度とメモリ効率を示しています。

核心コンセプト: 動的メモリを「データスタック」で管理。値はすべてインラインに配置され、スコープ終了時にポインタを戻すだけで一括解放されます。個別の free やデストラクタは存在しません。

項目内容
作者Wouter van Oortmerssen(FlatBuffers、Lobster の作者)
ライセンスApache License 2.0
コンパイル先C 言語(1 ファイルに出力、任意の C コンパイラでビルド可能)
バックエンドバンドルされた TinyCC(ビルドステップなしでインタプリタ的に実行可能)
公開日2026 年 8 月末(GitHub 公開)

主要な特徴

データスタックによるメモリ管理

Goose にはヒープがありません。代わりに、コンパイラが静的に割り当てる「データスタック」が複数存在します。 成長は単なるポインタの前進であり、スコープ終了時にポインタを戻すだけでメモリが解放されます。 100 万要素の構造体であっても、解放コストは 1 回のストアです。

for round in 3 {
    var scratch: u8[>..] = [];    // 成長専用配列
    for i in 100000 { scratch.push((i % 256) as! u8); }
    print("round ", round, ": ", scratch.len, " bytes");
}                                 // スコープ終了で 1 ストアで解放

成長しても参照は無効化されない

通常の Vec や vector は再アロケーションで要素への参照が無効になりますが、Goose のデータスタックでは値が移動しないため、 配列を成長させても要素への参照は有効なままです。

struct Item { id: i32, weight: f32 }

var items: Item[>..] = [];
let first .= items.push(Item { 1, 0.5 });   // 要素 0 への参照を取得
for i: i32 in 2..1000001 { items.push(Item { i, 0.0 }); }
first.weight = 99.5;                          // 100 万回 push 後も有効

ライフタイム注釈なしのメモリ安全性

Goose はすべての参照とスライスに静的な「ルート」(対象の生存期間を規定する変数)を推論します。 参照が所有者より長生きする可能性がある場合、コンパイラはエラーを出します。 &'a も unsafe も不要です。

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

var outer: u8[>..] = [];
var w = outer[..];
{
    var inner: u8[>..] = [];
    w = longest(outer, inner);   // エラー: inner をルートとする参照を
}                                //       より長生きの変数 w に格納

ネストデータの完全インライン化

文字列、文字列の配列、可変長フィールドを持つ構造体の配列も、すべて 1 つの連続したバイト列として格納されます。 ポインタによる間接参照は発生しません。

言語サイズアロケーション数
Goose29 バイト0
C++(std::string + std::vector)160 バイト1
Rust(String + Vec)153 バイト4

2 種類の Enum 表現

代数データ型(ADT)は Goose で唯一の動的ポリモーフィズムです。固定モードはタグ+最大ペイロード領域、 可変モード(Shape..)は各値がバリアントの実サイズだけを消費します。

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; }, _ => {} }
}

ポインタより狭いリンク(相対参照)

T&<u32> のような相対参照は、ターゲットへのオフセットを 1, 2, 4 バイトで持ちます。 これにより構造体はポジション非依存となり、ファイルにそのまま書き出せます。 読み込み時は検証パスで不正データを拒否できます。

共有メモリのないスレッド

ワーカーは独自のメモリ空間を持つ別プログラムとしてコンパイルされます。 スレッド間のデータ交換は、型付きキューを通じた memcpy のみです。 データ競合、ロック、アトミック、メモリオーダリングの概念が言語に存在しません。

多値リターンと長距離リターン

return err from load は、任意の階層上の load 関数からリターンします。 スタティックに検証され、アンワインディングや Result 型、? 演算子は不要です。

配列型ファミリー

Goose には単一の配列型ではなく、「サイズの変化方法」で区別された配列型ファミリーがあります。

型意味
T[k]固定サイズ(コンパイル時に決定)
T[], T[varint]構築時にサイズ決定、構築後は不変
T[..k], T[..]インライン容量内で伸縮可能
T[>..]成長専用(最も一般的)。参照は成長後も有効
T[>..<]成長・縮小両方(スタック、キュー、ヒープ向け)
T[:]スライス:任意の配列からのビュー(コピーなし)

文字列は u8 配列です。u8[>..] はビルダー、u8[] は完成文字列、 u8[:] はビューとして機能します。

ベンチマーク概要

16 のベンチマークの幾何平均(公式 README より)。Goose は速度とメモリの両方で優位性を示しています。

速度比較(対各言語の倍率、大きいほど Goose が速い)
idiomatic C++
3.3x
hand-optimized C++
1.16x
best safe Rust
1.12x
ピークメモリ比較(対各言語の削減率、大きいほど Goose が少ない)
idiomatic C++
1.9x
hand-optimized C++
1.3x
best safe Rust
1.2x

※ これらは公式ベンチマークスイートの結果です。すべてのユースケースで同様の結果が得られるとは限りません。 公式リポジトリには「負けたケース」も公開されています。

Goose 特性を活かしたサンプルコード

サンプル A: インライン注文帳
struct Item {
    sku: u8[varint],
    qty: varint,
    cents: varint,
}

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

fn main() {
    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 },
        ],
    });
    print(book);
}
29 バイト・0 アロケーションで 1 つの連続ブロックとして格納されます。 キャッシュ効率とシリアライズのオーバーヘッドが最小限です。
サンプル B: 成長中も有効な参照
struct Task { name: u8[>..], done: bool }

fn main() {
    var tasks: Task[>..] = [];
    let first .= tasks.push(Task {
        name: "設計", done: false
    });

    for i in 2..101 {
        tasks.push(Task {
            name: str("task", i), done: false
        });
    }

    first.done = true;   // 100 回 push 後も有効
    print(tasks.len);    // 100
}
push が返す参照はデータスタック上で移動しないため、 配列成長後もダングリングしません。
サンプル C: 可変モード Enum
enum Shape {
    Circle { r: f64 },
    Rect   { w: f64, h: f64 },
    Dot,
}

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 }

fn main() {
    var shapes: Shape..[>..] = [];
    shapes.push(Shape.Circle { r: 1.0 });
    shapes.push(Shape.Rect   { w: 2.0, h: 3.0 });
    shapes.push(Shape.Dot);

    for s in shapes {
        print(area(s));
    }
}
Shape.. は各バリアントを実サイズで保持。 Rust の enum と比較して最大 4 倍のメモリ削減と 2 倍の速度向上が期待できます。
サンプル D: 相対参照ツリー
struct Node {
    key: i32,
    left:  Node&<u32>?,
    right: Node&<u32>?,
}

fn main() {
    var nodes: Node[>..] = [];
    let root .= nodes.push(Node {
        key: 10, left: null, right: null
    });
    let left .= nodes.push(Node {
        key: 5, left: null, right: null
    });
    root.left = left;   // u32 オフセットでリンク

    print(nodes.len);   // 2
    print(root.left?.key);
}
T&<u32>? は 4 バイトの相対オフセット。 構造体全体をファイルに書き込んでもポジション非依存で読み戻せます。
サンプル E: 長距離リターン
fn load(path: u8[:]) -> u8[] {
    let data = read_file(path);
    guard data.len > 0 else {
        return "empty file" from load;
    }
    parse(data);
    return "ok";
}

fn parse(src: u8[:]) {
    let comma = find(src, ",");
    guard comma >= 0 else {
        return "expected comma" from load;
    }
    // ...
}
return ... from load は load から直接リターンします。 中間関数のシグネチャを汚さず、Result や ? も不要です。
サンプル F: 共有メモリなし並行処理
struct Job { n: i64 }
struct Result { n: i64, digits: i32 }

fn worker() {
    loop {
        let job = qget<Job>();
        guard job.n >= 0 else { break; }
        qput(Result {
            n: job.n,
            digits: count_digits(job.n),
        });
    }
}

fn main() {
    spawn worker();
    qput(Job { n: 12345 });
    let res = qget<Result>();
    print(res.digits);   // 5
}
ワーカーは独立したアドレス空間を持ち、型付きキュー経由で memcpy 通信します。 ロック、アトミック、データ競合の概念がありません。

制限事項とトレードオフ

Goose を使う上で知っておくべき制約
  • ヒープがない: Box、Rc、循環構造体、一般的な共有グラフは直接表現できません。代わりに相対参照やプールを使用します。
  • 可変長要素の配列はシーケンシャルアクセスのみ: インデックスアクセスはできません(別途インデックス配列が必要)。
  • スコープに縛られたライフタイム: データの生存期間は基本的に「構築した関数」までです。それを超える永続化には reusable プールが必要です。
  • 言語の語彙が豊富: guard、block、checkfree、reusable、from_bytes など、学習すべき概念が多数あります。
  • エコシステムが初期段階: 2026 年 10 月時点でまだ新しい言語であり、ライブラリやツールチェーンは今後の発展に依存します。

まとめ

Goose は、「ヒープなし・GC なし・ライフタイム注釈なし」という大胆な設計選択を通じて、 システムプログラミングにおけるメモリ管理とデータレイアウトの常識を書き換えようとする言語です。

データスタックによる一括管理、インライン化されたネスト構造、可変モード Enum、相対参照、 共有メモリなしの並行モデルなど、各機能が一貫したメモリ哲学の上に構築されています。 ゲームエンジン、シリアライゼーション、高頻度トレーディングなど、 キャッシュ効率とメモリレイテンシが重要な領域で特に強みを発揮する可能性があります。

一方で、ヒープの不在は表現力とのトレードオフであり、大規模コードベースでの長距離リターンの可読性や、 エコシステムの成熟度は今後の課題です。公式ドキュメントが充実しており、 チュートリアル・リファレンス・仕様書・31 のサンプルプログラムが揃っているため、 興味のある開発者はすぐに試すことができます。