この記事は、Sinclair Target氏が8月に書いた「Thoughts on Go vs. Rust vs. Zig」というブログ記事を紹介する。

元記事では、Go・Rust・Zigという3つのシステムプログラミング言語の設計思想とトレードオフの違いについて詳しく紹介されている。
言語を「機能」ではなく「価値観」で見る
筆者は、自分がこれまで「仕事で使われているから」という理由で言語を選んできたことに気づき、ここ数カ月で仕事では使っていない言語──Go、Rust、Zig──を意識的に触ってきたという。目的は習熟ではなく、「それぞれの言語が何に向いているのか」という感触をつかむことだ。
プログラミング言語は無数の軸で違いがあり、ただ「トレードオフがある」と言っても当たり前すぎて役に立たない。重要なのは「なぜこの言語はこのセットのトレードオフを選んだのか」という問いである、と筆者は書く。
- 機能リストから言語を選びたくはない
- 言語が選んだトレードオフの背後には、その言語が体現する「価値観」がある
- その価値観が自分にとってしっくりくるかどうかを知りたい
Go、Rust、Zigは機能セットがそれなりに重なっており、「Go vs Rust」「Rust vs Zig」といった比較記事やQ&Aが大量にあることからも、開発者が混乱しがちな領域だと指摘する。そこで筆者は、自分の経験からそれぞれの言語が何を重視し、どれくらいうまく実現できているかについて、大づかみな「判決」をまとめている。
Go: 極端なミニマリズムと「企業コラボレーション」のための言語
筆者がまず取り上げるのはGoである。Goの特徴は徹底したミニマリズムだとする。しばしば「モダンなC」と形容されるが、その意味するところは次のようなものだ。
- ガーベジコレクションとランタイムを持つためCそのものではない
- しかし「言語全体を頭の中に入れておける」という点でCに似ている
この「頭に入るサイズ」であることを実現するために、Goはあえて多くの機能を持たない。代表例として挙げられているのがジェネリクスで、Go 1.18で導入されるまでに約12年かかっている。また、近年の多くの言語が備えるタグ付きユニオンや、エラーハンドリング用のシンタックスシュガーのような機能も、Goには存在しない。
その結果として:
- 他言語なら短く書ける処理でも、Goではボイラープレートが増えがち
- しかし、その代償として言語仕様は長期にわたり安定し、読みやすさが確保されている
スライス型に現れる「少ない機能で頑張る」設計
具体例として、Goのslice型が取り上げられている。RustやZigにもスライスはあるが、そちらはいわゆる「太いポインタ」としてのスライス機能にとどまる。一方、Goのスライスは次のような性質を持つ。
- 連続したメモリ領域へのポインタ(太いポインタ)として振る舞う
- かつ、サイズを伸長できる(可変長配列的に使える)
そのため、GoのスライスはRustのVec<T>やZigのArrayListの役割も兼ねている。そしてGoではメモリを自分で管理しないため、
- スタックに置くかヒープに置くかはGoランタイムが決める
- RustやZigでは、このあたりをプログラマ自身がずっと気にすることになる
という違いが強調されている。
C++へのフラストレーションから生まれた言語
Go誕生の背景として、筆者は「Rob Pike氏がC++のコンパイル待ちと、C++コードでバグを埋め込む同僚たちにうんざりした」という起源譚に触れている。そこからGoは次のように設計されたという整理だ。
- C++がバロックに肥大化していくのに対し、Goは簡潔さを重視
- 「プログラマの大多数」にとって理解しやすく、90%程度のユースケースをカバーできることを優先
- 特に並行プログラミングを、平均的なエンジニアでも読み書きしやすい形で提供する
筆者自身は仕事でGoを使っていないが、「本来なら使うべきだろう」と述べている。Goのミニマリズムは、個人の楽しさというより「企業内でのコラボレーション」を支えるためのものだ、と結論づけているのが印象的である。
Rust: 安全性と性能を同時に取りに行くマキシマリスト
Goがミニマリストなら、Rustはマキシマリストだと筆者は対比させる。Rustには「ゼロコスト抽象化」という有名なタグラインがあるが、「ゼロコスト抽象化“かつ”その抽象が山ほどある」と言い換えるべきだと述べている。
Rustが難しい言語だ、という評判について、筆者は「ライフタイムの概念が難しいからではなく、とにかく詰め込まれている概念の数が多いからだ」というJamie Brandon氏の見解に賛同する。その例として、標準ライブラリ内部のPin<&LocalType>やCoerceUnsizedに関するコメントが引用されており、型・トレイト・ポインタ属性が幾重にも絡み合う「概念の密度」が示されている。
「安全性」とは何か、そして未定義動作という悪夢
Rustが複雑になる根本理由として、筆者はRustが
- 高い性能
- 強い安全性
という二つのゴールを同時に達成しようとしている点を挙げる。ここでいう「安全性」とは単にメモリ安全性(不正なポインタ参照や二重解放を防ぐ)に留まらず、「未定義動作(Undefined Behavior; UB)」を避けることまで含んでいる。
筆者はUBを「単にクラッシュするよりひどい状態」として描写する。エラーが検出されて即座にプロセスが落ちてくれればまだ良いが、検出されずに進んでしまうと、
- スレッド間のデータレースのタイミング
- その時点でメモリ上にたまたま載っていたガベージ
といった偶然に挙動が左右される「薄暗い世界」に突入し、再現性のないバグ(いわゆるHeisenbug)や深刻なセキュリティホールにつながる、と説明している。
Rustは、このUBを実行時コストなしで防ぐため、できる限りコンパイル時チェックで検出しようとする。そのために:
- コンパイラがコードの挙動を十分に理解できるよう、豊かな型システムと大量のトレイトを用意
- 開発者は、他言語なら「実行時の見かけの挙動」として済ませてしまう情報まで、型とトレイトを通じてコンパイラに明示する必要がある
という構造になっている。
「とりあえず書く」のではなく「コンパイラの言葉で書く」
この設計がRustの難しさにもつながる。Rustでは、直感的なコードを書いてからエラーが出たら直す、というより、
- まず「Rustがこのパターンに付けている名前」──対応するトレイトや型──を探す
- それに沿う形でコードを書く
という手順を踏まざるを得ない場面が多い。だが、その見返りとしてRustは他の言語では得がたい強い保証を提供する。
- 自分の書いたコードについて、メモリ安全性やUBの不在をかなり厳密に保証できる
- 同様に、ライブラリ側のコードについても保証が働くため、依存ライブラリを安心して利用しやすい
その結果として、RustのプロジェクトはJavaScriptエコシステムに匹敵するほど依存パッケージが増えがちだと指摘している点も興味深い。
Zig: 手動メモリ管理とデータ指向設計を前提にした「自由な」言語
3つの言語の中で、Zigは最も若く成熟度も低い。執筆時点でバージョン0.14であり、標準ライブラリはほとんどドキュメントがなく、使い方を学ぶにはソースコードを直接読むのが一番だと筆者は述べる。
筆者の見立てでは、ZigはGoとRustの両方への「リアクション」として理解できる言語である。
- Goは、コンピュータの動きをかなり抽象化して「やさしさ」のために多くを隠す
- Rustは、安全性のために多数の「お作法」を要求する
- Zigは、そうした抽象や規範から開発者を解き放とうとする
メモリアロケータを自分で選び、1バイト単位で管理する
GoやRustでは、構造体へのポインタを返すだけでヒープ確保が暗黙に行われる。対してZigでは、
- すべてのメモリ確保を明示的に記述する(手動メモリ管理)
alloc()を呼ぶ際には、どのアロケータ実装を使うかを自分で選ぶ必要がある
など、C以上に「メモリアロケーションを意識させる」設計になっていると説明している。
また、Rustで可変なグローバル変数を作ることが非常に難しく、フォーラムで長い議論が行われているのに対し、Zigでは単に宣言するだけで済むと比較している。ここにも、「開発者に強い制約を課さない」という姿勢が現れている、という評価だ。
「illegal behavior」とリリースモード
Zigも未定義動作を重く見ているが、アプローチはRustと異なる。Zigではこれを「illegal behavior」と呼び、実行時チェックによって検出し、発生時にはプログラムをクラッシュさせる。そのうえで、ビルド時に選べる4種類のリリースモードを用意し、
- チェックを有効にしたモードで十分に実行してバグを洗い出す
- 最終的な本番ビルドではチェックを外し、オーバーヘッドを削る
という pragmatism に基づく運用が想定されていると解説している。
OOPをさらに一歩遠ざけた「データ指向設計」の言語
Zigのもう一つの特徴は、オブジェクト指向との距離感である。GoもRustもクラス継承は持たないが、それでもメソッドやインターフェース/トレイトを通じて、オブジェクト同士のやり取りを中心とした設計は可能だ。
Zigはそこからさらに一歩踏み込む。
- メソッドはあるが、構造体のプライベートフィールドはない
- 実行時ポリモーフィズム(ダイナミックディスパッチ)を直接支える言語機能も持たない
- たとえば
std.mem.Allocatorは、まさにインターフェースになりたそうな型だが、あえてそうしていない
こうした制限は意図的なものであり、Zigは「データ指向設計(data-oriented design)」を推奨する言語だと筆者は位置づけている。
手動メモリ管理と「オブジェクト指向撤退」の関係
2025年にもなって手動メモリ管理の言語を作るのは奇妙に見えるかもしれないが、それはOOPを避ける設計と表裏一体だと筆者は説明する。
- GoやRust(そして多くのOOP寄り言語)では、「オブジェクトのグラフ」を構築し、各オブジェクトごとに小さな
mallocとfreeが裏側で多数走る - これはRAII的なライフタイム管理に近く、無数の小さな生存期間が入り乱れる
一方、Zigでは、オブジェクトにライフタイムを紐づけない前提に立つことで、
- イベントループ1回分といった「適切な単位」で大きなメモリブロックを確保し
- その中に扱いたいデータを詰め込み、適切なタイミングでまとめて解放する
というスタイルを推奨する。これが、Zigが開発者に求める「データ指向の考え方」であり、Rustとの差を生む本質的なポイントだと論じている。
「企業階層をぶっ壊す」ようなサブカル感
筆者は最後に、Zigに対してかなり情緒的な評価も与えている。Zigは
- 「企業的なクラス階層(オブジェクトのヒエラルキー)をぶっ壊すための言語」
- 「大いなる独裁者気質やアナーキストのための言語」
といった、やや過激でユーモラスな比喩で表現されている。実装面では、Zigチームが現在、依存ライブラリをすべて自前で書き換えようとしていることに触れ、「Linuxカーネルを全部Zigで書き直してから1.0を出すのではないか」と半ば冗談めかして締めくくっている。
3つの言語が体現する価値観
記事全体として、筆者はGo・Rust・Zigを単なる機能比較ではなく、「どんな価値観を体現しているか」という観点から整理している。
Go
- 言語仕様を極力小さく保ち、読みやすさと安定性を優先
- 企業内で多数のエンジニアが共同開発する現場に最適化されたツール
Rust
- 安全性と性能を妥協なく追求するため、豊富な抽象と概念を受け入れたマキシマリスト
- コンパイラが理解できる形で意図を明示する代わりに、強い保証を得る世界
Zig
- 抽象やOOPから距離を取り、開発者に極力多くの制御権を返す
- 手動メモリ管理とデータ指向設計を前提とした、「サブカル感」のある低レベル言語
どの言語も同じ問題領域──ネイティブコンパイルされるシステム寄りのツールやサービス──を狙っているが、それぞれがまったく異なる価値観を押し出している点が、この3つを比較するおもしろさだといえる。
筆者は、自分の偏見を意識的に「結晶化」させる試みだと断りつつも、こうした大づかみな見取り図は、言語選択に悩むエンジニアにとって有用なレンズになると示唆している。
詳細はThoughts on Go vs. Rust vs. Zigを参照していただきたい。