はじめに
2026年10月下旬に新しいプログラミング言語「Goose」が公開されました.
公式ドキュメントや仕様書を読んで整理したメモ用の記事になります.
普段はRustを書いており(最近はAIにやらせておりほとんど書いていないが),Gooseの紹介記事にRustとの比較があったため,気になって調べてみました.
公式が掲げている主な特徴は次のとおり.
- メモリ安全で、
unsafeも未定義動作もない - ヒープ,GC,参照カウント,デストラクタがない
- ライフタイム注釈を書かなくてよい
- 出力はCのソースファイル1つで,そこから実行ファイルを作る
- 作成者のベンチマークでは,C++やRustより速くかつメモリも少ない
Gooseの概要
| 項目 | 内容 |
|---|---|
| 作者 | Wouter van Oortmerssen |
| 実装 | コンパイラは約3万行のC++ |
| 開発体制 | 言語,コンパイラ設計は人間だが,コンパイラ実装・ベンチマーク・サンプル・ドキュメントはほぼAIが生成したとのこと |
| 型 | 静的型付け,C風の構文,ジェネリクスは単相化 |
メモリモデルはデータスタック
構成
Gooseが使うメモリは.
- ネイティブのコールスタック
- 静的データ
- データスタック N本(Nはコンパイラが決める)
の三つで,ヒープは存在しません.
データスタックは巨大なアドレス予約+bumpポインタで,アリーナアロケータに近い.Rustで言えば,すべての Vec や String がスコープ付きのアリーナ(bumpalo のようなもの)に自動で載るイメージ.
ランタイムは起動時にデータスタック1本あたり2GBの既定値のアドレス空間を予約して,物理メモリは実際に書き込んだページにだけ割り当てます.
Vecで見る違い
Rustの Vec はヒープ上のバッファを指すポインタと容量・長さで構成されます.容量が足りなくなるとリアロケートして,要素を別の場所へ移動させます.
Gooseでは動的な値(T[>..] など)は必ずデータスタックの最上部に置かれます.
- 1本のスタック上で動的な値は同時に1つで,常に最上部にある
- 上に何もないため
pushはポインタを進めるだけで済見,リアロケートも移動も起きない - 同時に2つ動的な値を確保する場合は,コンパイラが別々のデータスタックに割り当てる
解放
解放はスコープに入った時点の位置までポインタを戻すことで可能です.
fn foo() { データスタック #1
var a: u8[>..] = ...; [ a ........ ]▶
{ ← 位置を記録
var tmp: u8[>..] = ...; [ a ........ | tmp ...... ]▶
} ← 記録した位置に戻す(ストア1回)
[ a ........ ]▶ tmp だけ消える
}
- 解放コストはストア1回で
freeもDropの連鎖もない - 巻き戻した後の物理ページはOSに返さずに次のアロケートで再利用する
この仕様によるキャラクタ
参照が無効になりません.データが移動しないのでpush を何度しても既存の参照は有効です.
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; // 100万回push後も有効
Rustでは &v[0] を持ったまま v.push() はできませんがGooseではこれが合法なので,借用の排他ルールを必要としません
また,データがインラインに詰まります.
| 実装 | サイズ | 確保回数 |
|---|---|---|
| Goose | 29バイト | 0回 |
C++(std::string + std::vector) |
160バイト | 1回 |
Rust(String + Vec) |
153バイト | 4回 |
代償として,可変長要素の配列は先頭から順にたどることはできますが,添字で直接アクセスできません(別途参照の配列を索引として持てば可能).
ライフタイム注釈がない
すべての参照とスライスには静的な rootがあります.
コンパイラはrootを推論して,参照がrootより長生きしないことを検査するため,チェック内容はRustのライフタイムに近いですが 'a のような注釈は書きません.
var outer: u8[>..] = [];
var w = outer[..];
{
var inner: u8[>..] = [];
w = longest(outer, inner);
// error: storing a reference rooted at inner,
// which does not outlive the destination
}
ムーブがない
Rustで所有権エラーになるケース
// Rustのソース
let mut v = vec![1, 2, 3];
consume(v); // ムーブ
v.push(4); // error: v はムーブ済み
// Gooseのソース
var v: i64[>..] = [1, 2, 3];
show(v); // 参照として渡る(引数がスライス・参照・型なしの場合)
v.push(4); // OK
代入も同様でlet alias = a; は同じ配列への別名になります.
独立したコピーが必要な場合は copy(a) と明示します.
- 値渡しの引数(
fn f(xs: i64[>..]))に変数をそのまま渡すとエラーになる.f(copy(v))と書くか、その場で作った値を渡す - ムーブが起きるのは関数のローカル変数を
returnするときだけ出,その場合も戻り値は呼び出し側の置き場所に最初から作られる
値が移動せず,さらに構造体内のリンクは相対参照で表せてasync/Futureも存在しません.
そのためRustの Pin に相当する概念も不要です.
代わりにコンパイラが止めるもの
借用エラーに相当するチェックは,実際にメモリが失われる操作でされます.
let first .= v[0];
v.pop(); // error: v を指す first がまだ生きている
print(first);
- 縮める操作(
popやclear,resize)は,配列内を指す参照が生きている間は禁止 - 配列全体を再代入する場合,右辺はその配列自身を使えない(
x = f(x)はエラー)
並行処理
-
thread_fnで動くワーカーは,それぞれ独立したメモリを持つ別プログラムとして動く - やり取りは型付きキューだけで,渡せるのは参照を含まない値に限られる
- 公式いわく,データ競合・ロック・アトミック・メモリオーダリングは存在しない
- OSスレッドであり軽量スレッドではない.async/awaitや非同期I/Oの仕組みはない
Goのgoroutine+channelに近いと思いましたが,ユースケースはCPU並列の計算処理向けで大量の接続をさばくサーバー用途には向かないかもしれない.
ベンチマーク
作者が用意した16個のベンチマークの幾何平均です.
| 比較対象 | 速度(MSVC) | 速度(clang) | ピークメモリ |
|---|---|---|---|
| 慣用的なC++ | 3.51倍速い | 3.54倍速い | 1.95分の1 |
| 手で最適化したC++ | 1.24倍速い | 1.26倍速い | 1.32分の1 |
| 最速の安全なRust | 1.13倍速い | 1.19倍速い | 1.25分の1 |
ビルドと出力
| コマンド | 結果 |
|---|---|
goose -o x.c x.goose |
プログラム全体をCファイル1つに変換する |
goose build x.goose |
ランタイムとリンクして実行ファイル1つを作る(gcc / clang / MSVC が必要) |
goose x.goose |
同梱のTinyCCでその場でコンパイルして実行する |
- 生成したCは常に
-O2(MSVCでは/O2)でコンパイルされる,--debugを付けるとオーバーフロー・範囲・タグのチェックが有効になる -
--standaloneを付けると,ランタイムをプログラム内に埋め込む - スレッドを使うプログラムは
goose buildが必要 - コンパイルはプログラム全体を一度に行う方式
クロスプラットフォームとクロスコンパイル
- 公式の配布とCI対象は Windows(x64),Linux(x64,glibc 2.35以上),macOS(Apple Silicon / Intel,13.3以上)
- 出力が普通のCなのでCコンパイラがある環境なら原理的に動かせる
- クロスコンパイルは公式ドキュメントに記載がなく,Zigのように公式機能として整備されているわけではない?
おわりに
これからエコシステムも発展していくと思うので期待しています.
性能と安全性の主張も作成者の発表段階にあるため,現時点では本番利用を検討するというよりも設計思想を学ぶ対象として見ていますが,将来的にはこの言語を使って何か作ってみたいと思ってます.

