Zig 言語の調査

Zig がどんな言語か、どう書くか、どれくらい速いかを公開情報から調べる

📅 作成: 2026-10-18 / 更新: 2026-10-18

汎用のシステムプログラミング言語 Zig について、言語の特徴・構文・メモリ管理・エラー処理・並行処理・速度・採用例を調べた結果。

std.Io のコードは動かしていない。基本構文・メモリ管理・エラー処理は、Zig 0.17.0 で動くサンプルとテストで確かめた(syntax.zig ・ memory.zig ・ errors.zig。全件の実行は tools/40_test/run-tests.cmd)。並行処理の章の std.Io は 0.16 で入ったばかりで、版によって関数名や引数が変わりうる。

Zig の概要

Andrew Kelley 氏が 2016 年に始めた言語。非営利の Zig Software Foundation が開発する。C の置き換えを狙い、C と同じ領域をより安全・明快に書けるようにする。

特徴

現状

C ・ Rust との比較

CRustZig
メモリ安全性なしコンパイル時に保証(borrow checker)実行時チェックのみ
エラー処理戻り値・errnoResult<T, E>エラー共用体 E!T
学習コスト低高中
安定性高高1.0 前

基本構文

記号の意味

記号書く位置意味
:変数・引数の名前の後ろ型注釈(const a: i32 = 10;)。戻り値の型は ) の後ろに : なしで書く
E!T / !T型エラー共用体型。エラー集合 E のどれか、または T の値。E を省くとコンパイラが推論する
?T型optional。値か null
|x|if ・ while ・ for ・ catch ・ switch の後ろキャプチャ。取り出した値に名前を付ける。|*x| はポインタで受ける
A || B型エラー集合の合併
!a式論理否定
a | b式ビット OR
and / or式論理積・論理和。&& ・ || ではなく語で書く。右辺は必要なときだけ評価する
orelse / .?式optional の取り出し。null なら既定値/ .? は orelse unreachable と同じ

制御構文

const a: i32 = 10;                  // const は変えない、var は変える
const c = if (a > 5) "大" else "小"; // if は式になる(三項演算子の代わり)

const maybe: ?i32 = 42;
if (maybe) |v| { _ = v; }           // 値があるときだけ取り出す

var i: usize = 0;
while (i < 3) : (i += 1) {}         // : ( ) は毎回の後処理
while (it.next()) |item| { _ = item; } // null が返るまで回す

for (0..10) |n| { _ = n; }          // 範囲・配列・スライスを回す。C 形式の for は無い
for (arr, 0..) |x, idx| { _ = x; _ = idx; } // 要素と添字
for (arr) |*p| { p.* += 1; }        // ポインタで受けて書き換える

const s = switch (a) {              // 全部の値を網羅しないとコンパイルエラー
    0 => "zero",
    1...9 => "small",
    else => "big",
};
defer cleanup();                    // スコープを出るときに必ず実行する

関数

fn add(a: i32, b: i32) i32 { return a + b; }
fn parse(s: []const u8) !u32 { ... }             // エラーを返しうる
fn max(comptime T: type, a: T, b: T) T { ... }   // 型を引数で受ける(ジェネリクスの代わり)

メモリ管理

GC は無く、確保も解放も手動で行う。メモリを確保する関数は allocator を引数で受け取る。確保の仕方は呼び出し側が決める。

fn makeList(alloc: std.mem.Allocator, n: usize) ![]u32 {
    return try alloc.alloc(u32, n);   // 失敗すると error.OutOfMemory
}

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}){};
    defer _ = gpa.deinit();            // 終了時にリークを報告する
    const alloc = gpa.allocator();

    const list = try makeList(alloc, 10);
    defer alloc.free(list);            // 確保した直後に解放を書く
}

allocator の選び方

言語リファレンスの「Choosing an Allocator」に挙がっているもの。

allocator使いどころ
std.heap.c_allocatorlibc をリンクするなら、主な allocator の第一候補
std.heap.FixedBufferAllocator使う量の上限がコンパイル時に分かるとき。ヒープを使わない
std.heap.ArenaAllocatorコマンドラインのプログラムや、1 フレーム・1 リクエスト単位の処理。最後にまとめて解放する
std.heap.page_allocatorOS からページ単位で確保する。arena の土台に使う
std.testing.allocatorテスト用。簡単なリーク検出をする
std.testing.FailingAllocatorerror.OutOfMemory の扱いをテストする
std.heap.DebugAllocatorDebug ビルドの汎用。0.14 で GeneralPurposeAllocator から改名
std.heap.smp_allocatorリリースビルドの汎用

Rust との違い

Rust はコンパイル時に所有権を検査するが、Zig は検査しない。解放後の使用や二重解放は、Debug ビルドの実行時チェックや DebugAllocator で見つける。Bun が Rust へ移った主な理由もここにある(「Zig で書かれたプロジェクト」の章)。

エラー処理

例外は無い。エラーは戻り値として、エラー共用体型 E!T で返す。

const ParseError = error{ Empty, TooLong };  // エラー集合を定義する

fn check(s: []const u8) ParseError!void {
    if (s.len == 0) return error.Empty;
    if (s.len > 10) return error.TooLong;
}

fn run() !void {
    try check("abc");                  // エラーなら呼び出し元へ返す

    check("") catch |err| switch (err) { // その場で処理する
        error.Empty => {},
        error.TooLong => {},
    };

    const n = std.fmt.parseInt(u8, "x", 10) catch 0; // 失敗したら既定値
    _ = n;

    if (check("x")) |_| {} else |err| { _ = err; } // if でも分けられる
}

fn open(alloc: std.mem.Allocator) !*Res {
    const r = try alloc.create(Res);
    errdefer alloc.destroy(r);         // この後でエラーが返ったときだけ実行する
    try r.init();
    return r;
}
他言語の書き方Zig
throwreturn error.Xxx;
try { } catch { }x() catch |err| { ... }(式単位)
呼び出し元へ伝えるtry x()
finallydefer
失敗したときだけ後始末errdefer
意図して無視するx() catch unreachable

並行処理と非同期 I/O

コルーチン・goroutine・generator

ほかの言語の機能Zig では
コルーチン(言語の機能として)無い
Go の goroutine構文としては無い。近いのは std.Io の io.async() / io.concurrent()
JS の generator(yield)無い。next() を持つ構造体(イテレータ)を書く

async / await が外された理由

代わりの仕組み(0.16 の std.Io)

allocator を引数で渡すのと同じように、I/O を行う関数は io を引数で受け取る。並行に動かしたいところで io.async() を呼ぶと future が返り、future.await(io) で結果を受け取る。

fn work(io: std.Io) !void {
    var f = io.async(fetchA, .{io});
    defer f.cancel(io) catch {};   // 取り消しと後始末
    const r = try f.await(io);
    _ = r;
    try io.sleep(...);             // タイマ。待つのも io を通す
}
旧(〜0.10)新(0.16〜)
仕組み言語のキーワード標準ライブラリの関数
関数の種類同期関数と async 関数すべて普通の関数。io を引数で受けるだけ
実行のしかた言語が決める渡す Io の実装で選ぶ。std.Io.Threaded(スレッド)/ std.Io.Evented(io_uring ・ kqueue)

速度

実行速度

LLVM を最適化に使うため、ReleaseFast で作ったバイナリは C ・ Rust と同程度になる。2021 年の比較スレッドでは 6 本中 4 本で Rust が上回ったが、差は小さく、マイクロベンチマークでは優劣を決められないと結論している。速さを分けるのは言語ではなく、メモリ確保の仕方やデータ配置といった書き方である。

コンパイル速度

Debug ビルドは、LLVM の機械語生成に時間がかかり、遅いとされてきた。

処理系最小プログラムの Debug ビルド
Zig 0.13.0約 1.27 秒
rustc 1.78.0約 0.28 秒
clang++ 17約 0.16 秒

Zig の時間の大半は LLVM のオブジェクト出力だった(Ziggit の投稿者 1 名の計測)。

その対策として、Zig は次の 2 つを進めている。

再ビルドがミリ秒単位になったという報告や、75 秒が 20 秒に縮んだという報告がある。ただし二次情報で、計測の条件は確かめていない。2026 年 5 月には、ビルドシステムの改修で zig build -h が約 150 ミリ秒から約 14 ミリ秒になった。

数値は自分の環境で測り直す。この章の数値は版の古い計測や二次情報を含む。Zig は版ごとにコンパイラが大きく変わるため、使う版で hyperfine などを使って測らないと判断に使えない。

Zig で書かれたプロジェクト

プロジェクト何か補足
BunJavaScript/TypeScript のランタイム・バンドラ・パッケージマネージャZig で開発され、Zig の採用例として最も知られていた。2025 年 12 月に Anthropic が買収。その後 Rust に書き換えられ、2026 年 5 月に本体へ取り込まれた。約 53.5 万行の Zig を AI で移植し、主な理由は use-after-free などのメモリ安全性の不具合を防ぐこと。Zig 版の最後は 1.3.14
TigerBeetle金融取引(複式簿記)向けの分散データベースZig Software Foundation へ Synadia と共同で 512,000 ドルの寄付を表明
Ghosttyターミナルエミュレータ
Roc関数型言語のコンパイラRust から Zig へ書き直し
RiverWayland の動的タイル型コンポジタ
hermetic_cc_toolchainBazel 用の C/C++ ツールチェーンUber が管理。zig cc の上で C/C++ をクロスコンパイルする。Uber は arm64 対応の基盤に使った
ZigZig のコンパイラZig 自身で書かれている(セルフホスト)

出典