rust-dev について
Rust 初学者や CLI、サービス、ライブラリの開発を始める人向けの実践的なスキルです。所有権、エラー処理、初期設計の選択肢、一般的なイディオムを扱います。
主な機能
- 01Rustの所有権と借用の基礎
- 02エラー、トレイト、型の選択指針
- 03初期設定とクレート選び
- 04一般的なイディオムとコンパイラー主導のリファクタリング
活用シーン
- 01RustのCLI、サービス、ライブラリの開始
- 02他言語からRustへの移行学習
- 03型やエラー処理方針の選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tenequm/ skills --skill rust-devインストール後、Claude Code で/rust-devを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
CHANGELOG.mdLICENSE.txtreferences/async-basics.mdcrate-shortlist.mddev-environment.mderror-handling.mdownership-and-types.mdperformance.mdproject-shape.mdreleasing.mdtesting.mdtraits-and-generics.md詳細
Rust 開発 - 1 日目
最初のコミットから質の高い Rust アプリを書くための実践的な基礎です。教科書ではありません。他の言語との違い、ほかのすべてを左右する初日の判断、そして多くの実際のアプリをカバーする少数のクレートに焦点を当てます。
使用する場面
- Rust の新しいプロジェクト(CLI、サービス、ライブラリ)を始めるとき
- Python、JavaScript、Go、Java/C#、C++ から Rust に移行するとき
- 所有型と借用型、スマートポインター、トレイトオブジェクトとジェネリクスのどちらを使うか選ぶとき
- エラー処理の方針(
anyhowとthiserror)を決めるとき - どのクレートを使うか選ぶとき
- 最小限ながら明確な方針を持つ
Cargo.toml、clippy、rustfmt を設定するとき
初日のセットアップ
# 1. Install the toolchain (rustup is the toolchain manager)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Confirm components (rustfmt and clippy ship with stable, rust-src enables IDE features)
rustup component add rustfmt clippy rust-src
# 3. Create a project
cargo new my-app # binary (src/main.rs)
cargo new --lib my-lib # library (src/lib.rs)
# 4. The dev loop (memorize these four)
cargo check # fast type-check, no codegen
cargo run # build and run (binary)
cargo test # build and run tests (incl. doctests)
cargo clippy --all-targets # lint, incl. tests/ and benches/ (run before pushing)
cargo fmt # format
# 5. Manage dependencies without editing Cargo.toml by hand
cargo add tokio --features full
cargo remove tokio
cargo update # recompute Cargo.lock within existing semver ranges
cargo update は、Cargo.toml ですでに指定されているバージョン範囲内でのみ更新します。メジャーバージョン(1.x から 2.0)をまたぐには、Cargo.toml を編集するか、cargo add <crate>@2 を実行する必要があります。
rust-analyzer は必須です。 これはすべてのエディターが使用する言語サーバーです(VS Code、Zed、Neovim、Helix、RustRover は独自のエンジンを使用しますが、同等のものです)。VS Code では、rust-analyzer 拡張機能をインストールし、保存時にリントのフィードバックが得られるよう rust-analyzer.check.command を "clippy" に設定してください。
後でファイルウォッチャーが必要になったら? cargo install --locked bacon を実行し、プロジェクトで bacon を起動します。初日には必要ありません。
Rust のメンタルモデルを形作る 5 つの考え方
Rust は、多くの言語で当たり前と思われている 2 つのもの(ガベージコレクターと例外)を、メモリ、データ競合、エラー処理に関するコンパイル時の保証と引き換えにします。言語の構造は、このトレードオフから生まれています。
1. 所有権:すべての値には所有者が 1 つだけあります
値を、物理的なオブジェクトのように考えてください。本、ファイル、ネットワーク接続などです。どの時点でも、1 つの変数がそれを所有します。次のことができます。
- ムーブする:
let b = a;は所有権をbに渡します。aは使えなくなります。 - 不変に借用する:
&aによって、ほかの人が値を参照できます。複数の読み取りが可能です。 - 可変に借用する:
&mut aによって、1 つの人が値を変更できます。アクセスは排他的です。 - クローンする:
a.clone()はディープコピーを作成します。それぞれが独自の値を保持します。
所有者がスコープを抜けると、値はドロップされます(メモリが解放され、ファイルが閉じられ、ロックが解除されます)。GC も手動の free もありません。これは RAII であり、コンパイラーによって強制されます。
2. エイリアス XOR 可変性
どの時点でも、データの一部が持てる参照はどちらか一方です。
- 可変参照(
&mut T)が 1 つ、または - 不変参照(
&T)が任意の数、
両方を同時に持つことはできません。この単一のルールによって、データ競合と多くの解放後使用バグがなくなります。借用チェッカーがこれを強制します。エラーが出たとき、それは言語が難しくしているのではなく、データの所有権についての考え方が明確でないことを示しています。
3. エラーは値であり、例外ではありません
try/catch はありません。失敗する可能性がある関数は Result<T, E> を返します。有用な値を返せない関数は Option<T> を返します。コンパイラーは、どちらも必ず処理するよう求めます。? 演算子を使えば、1 文字で呼び出しスタックの上位へエラーを伝播できます。
fn read_config() -> Result<Config, anyhow::Error> {
let text = std::fs::read_to_string("config.toml")?; // ? = early-return on Err
let config = toml::from_str(&text)?;
Ok(config)
}
null はありません。Option<T> は None または Some(value) です。コンパイラーは None のケースを忘れることを許しません。
4. トレイトは Java のインターフェースとは異なります
trait は振る舞いを定義します。型はトレイトを impl します。ここまではおなじみでしょう。違いは次のとおりです。
- 静的ディスパッチがデフォルトです。
fn f<T: Display>(x: T)と書くと、コンパイラーはfを呼び出した具体的なTごとに別々のコピーを生成します(C++ のテンプレートと同様の単相化)。実行時のオーバーヘッドはありません。 - 動的ディスパッチは
dyn Traitで明示的に選択します(通常はBox<dyn Trait>または&dyn Trait)。呼び出しごとに vtable の検索が 1 回発生します。 - 継承はありません。 トレイトは合成して使います。型を「拡張」するために
Derefを使いたくなったら、いったん立ち止まり、合成または enum を使ってください。 - 孤児ルール:
impl YourTrait for SomeoneElsesTypeまたはimpl SomeoneElsesTrait for YourTypeはできますが、両方が外部の型・トレイトである場合はできません。これにより、依存関係の解決が適切に行われます。
5. 借用チェッカーは設計の指針です
初心者が最もよく陥る間違いは、コンパイラーエラーを黙らせるべき障害として扱うことです。そうではありません。借用チェッカーのエラーは、ほぼすべてが何を誰が所有するかに関する実際の問題を示しています。行き詰まったとき、問うべきことは「どうすればコンパイルできるか」ではなく、ほぼ必ず「ここで本当に望んでいる所有権の関係は何か」です。エラーを読みましょう。コンパイラーは驚くほど詳しく説明してくれます。
すべての関数シグネチャで確認する3つの質問
関数を書く前に、入力を所有する必要があるのか、読み取る必要があるのか、変更する必要があるのかを確認しましょう。
fn consume(s: String) // owns: function takes responsibility, caller loses it
fn read(s: &str) // reads: function looks at it, caller keeps it
fn modify(s: &mut String) // mutates: function changes it in place
90%のケースでうまくいくデフォルト:
- 関数のパラメーター:
Stringより&str、Vec<T>より&[T]を優先します(これらはスライスで、所有権を持つ呼び出し元と借用する呼び出し元の両方を受け入れます)。 - 関数の戻り値:所有権を持つ型(
String、Vec<T>)を返します。参照を返すにはライフタイムが必要になるため、必要になるまでは避けましょう。 - 構造体のフィールド:所有権を持つ型(
String、Vec<T>)を優先します。構造体に&strを格納するのは、初心者が最もよく陥る落とし穴です。その構造体を保持するすべての型にライフタイム注釈が必要となり、問題が連鎖します。
初日に使う判断表
最初に直面する選択肢への一行回答です。
| 判断 | デフォルト | もう一方を選ぶ場合 |
|---|---|---|
Stringと&str(構造体のフィールド) | String | 本当に理由があり、ライフタイムを理解するまでは、ほとんどの場合&strは選びません |
Stringと&str(関数のパラメーター) | &str | 内部で所有または保存する必要がある場合に限りStringを使います |
Vec<T>と&[T](パラメーター) | &[T] | 所有する必要がある場合に限りVec<T>を使います |
Box<T>とRc<T>とArc<T> | Box<T>(単一の所有者、ヒープ) | スレッド間で所有権を共有する場合はArc<T>を使います。デフォルトとしてのRc<T>は避けましょう。非同期処理に移行したときにリファクタリングせずに済むよう、Arc<T>を使います |
RefCell<T>とMutex<T> | Mutex<T>(またはRwLock<T>) | 同じ理由です。RefCellは非同期処理やスレッドでは使えませんが、こちらは使えます |
Option<T>とResult<T, E> | 「値がない」場合はOption<T>、「理由があって失敗した」場合はResult<T, E> | 値がないことに呼び出し元が処理すべき意味がある場合はResult |
dyn Traitとimpl Trait / <T: Trait> | ジェネリック(<T: Trait>またはimpl Trait)- 静的ディスパッチ | 異種コレクション(Vec<Box<dyn Animal>>)が必要な場合はBox<dyn Trait> |
| アプリケーションコードのエラー | どこでもanyhow::Result<T> | - |
| ライブラリーコードのエラー | thiserrorから派生した列挙型 | 公開ライブラリーAPIではBox<dyn Error>を使いません。呼び出し元にダウンキャストを強制するためです |
&selfと&mut selfとself | ゲッターには&self、セッターには&mut self、ビルダーや所有権を消費する操作にはself | - |
| モジュールの構成 | ファイルが長くなるまではインラインモジュールを使い、その後分割します | 1モジュール=1ファイルという考え方はJava/C#の発想であり、Rustの発想ではありません |
早いうちに身につけたいイディオム
これらは、ほぼすべての Rust プログラムに登場します。最初の週に覚えましょう。
エラー伝播には ? を使います。 9 行の match を 1 文字に置き換えられます。
let body = reqwest::get(url).await?.text().await?;
手書きのループよりイテレータチェーンを使います。 手書きのループと同じ機械語にコンパイルされます(LLVM がクロージャをインライン化します)。Rust らしいコードは関数型のスタイルです。
let active_emails: Vec<String> = users
.iter()
.filter(|u| u.active)
.map(|u| u.email.clone())
.collect();
match の網羅性。 enum にバリアントを追加すると、それを処理していない match はすべてコンパイルエラーになります。これを活用しましょう。これは、あらゆる言語で最も強力なリファクタリングツールの 1 つです。
リファクタリングはコンパイラーに導いてもらいましょう。 同じ手法は構造体にも使えます。Default のない必須フィールドを追加すると、既存のコンストラクターがすべてコンパイルエラーになります。更新が必要な箇所を漏れなく示す一覧を、無料で得られます。
よくある単一アームの match には if let と let else を使います。
if let Some(name) = user.name { println!("hi {name}"); }
let Some(name) = user.name else { return Err(anyhow!("no name")); };
// `name` is in scope from here on, no nesting
型変換には From / Into を使います。 From を実装すれば、Into も自動的に使えます。? は From を使ってエラー型を自動的に変換します。
Option / Result のコンビネーターを使います。 match を使う前に、.map、.and_then、.unwrap_or、.unwrap_or_else、.ok_or を検討しましょう。
derive マクロを使います。 #[derive(Debug, Clone, PartialEq)] を使えば、定型コードの 80%を手間なく用意できます。JSON には #[derive(Serialize, Deserialize)] を追加します。
最近追加された糖衣構文。 知っておく価値のある安定化済み機能を、新しいものから紹介します。bool::ok_or と ok_or_else(1.98)を使うと、条件から直接 Result を生成できます(ready.ok_or(Error::NotReady)?)。cfg_select! は cfg 条件をコンパイル時に match するマクロで、cfg-if クレートの用途の大半を置き換えます。また、if let ガードを match アームで使えます(match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... })。どちらも 1.95 で安定化されました。assert_matches! と debug_assert_matches!(1.96)は、等価性ではなくパターンに対してアサートします。これは enum に自然なアサート方法です。よく見かける古い機能も 2 つあります。珍しい機能と勘違いしないようにしましょう。let チェーン(if let Some(x) = a && x > 3 { ... })は 1.88 から edition 2024 で安定化されており、非同期クロージャ(async || { ... }。AsyncFn 系のトレイト境界を含みます)は 1.85 から安定化されています。
Xから来た場合に戸惑う点
PythonまたはJavaScriptから来た場合:
- ヒープ値(
String、Vecなど)に対するlet b = a;は、値をムーブします。aは以降使えなくなります。借用するには&aを、コピーするにはa.clone()を使います。 - nullはありません。
Option<T>の使用が必須です。 - 例外はありません。
Result<T, E>と?を使います。コンパイラーはエラーを無視させません。 - 継承はありません。コンポジション、トレイト、列挙型を使います。
- 変数はデフォルトで不変です。変更するには
mutを付けます。参照も同様に、&と&mutを使い分けます。 - 同じ名前を再宣言することができ、慣用的な書き方です。
let input = input.trim();のように、古い束縛を新しい束縛でシャドーイングできます。新しい束縛は別の型でもかまいません。これは変更ではなく、input2を作らずに値をその場で変換するRustの方法です。 - 整数型は明示する必要があり、インデックスには
usizeが必要です。また、ビルドの種類によってオーバーフロー時の動作も異なります。デバッグビルドでは「実行時にプログラムを パニック させる整数オーバーフローのチェックが含まれます」が、--releaseではチェックがなくなり、「2の補数によるラップアラウンド」が行われます。テストスイートに合格しても、本番環境ではラップアラウンドが起こる可能性があります。そのため、デバッグ時のパニックによる検出に頼るのではなく、演算が実際に限界値に達する可能性がある場合はchecked_*/saturating_*/wrapping_*を使ってください。strict_addなど(1.91から安定版)は、すべてのビルドでオーバーフロー時にパニックします。また、[profile.release]でoverflow-checks = trueを設定すると、本番環境でもデバッグ時と同じ動作になります。整数型間のasによる変換では、値が黙って切り詰められます(300_u32 as u8は44になります)。値が収まるとは限らない場合は、u8::try_from(x)?を使ってください。 Stringにはインデックスでアクセスできません。s[0]はコンパイルできません。代わりに何を使うべきかはreferences/ownership-and-types.mdを参照してください。
Goから来た場合:
- エラーを値として扱います。同じ感覚で使えますが、
if err != nilの代わりに?を使います。 nilはありません。Option<T>を使います。- GCもゴルーチンもありません。所有権と借用を使い、
tokioでasync/awaitを使います。非同期モデルは協調的です(awaitは明示的な譲渡ポイントです)。プリエンプティブではありません。 interface{}はトレイトになります。静的ディスパッチにはジェネリクスをデフォルトで使い、必要な場合にのみBox<dyn Trait>を使います。- 静的リンクがデフォルトです。バイナリは大きくなりますが、単体で動作します。
- アプリケーションコードでは、復旧不能なバグに限って
panic!を使ってください。Goのように「ログを出して処理を続ける」目的では使わないでください。
JavaまたはC#から来た場合:
- デフォルトでは、トレイトは仮想ディスパッチを行うインターフェースではありません。
<T: Trait>は単相化されます。動的ディスパッチを使う場合はdyn Traitを明示します。 - null参照はありません。
Option<T>を使います。 - 例外はありません。
Result<T, E>と?を使います。 - クラス継承はありません。直和型には列挙型を、共通の振る舞いにはトレイトを使います。
- GCはありません。ライフタイムは所有権と借用によって決まります。
Arc<T>はJavaの参照に最も近いものです。 - ジェネリクスは単相化され、型消去はされません。
C++から来た場合:
- RAIIと似ていますが、借用チェッカーがコンパイル時にそれを保証します。
- コピー/ムーブコンストラクターはありません。
Cloneは明示的で、Copyは低コストなビット単位のコピーを示すマーカートレイトです。 - 安全なコードでは未定義動作はありません(理論上)。
&はコンパイル時にチェックされる借用であり、生ポインターではありません。生ポインター(*const T、*mut T)もありますが、デリファレンスにはunsafeが必要です。- スマートポインターには、
Box<T>(unique_ptr)、Rc<T>(shared_ptr、単一スレッド)、Arc<T>(shared_ptr、スレッドセーフ)があります。 - マクロには衛生性があります。
serdeやtokio::mainなどは、手続き型マクロ(derive、属性、関数形式)によって動作します。
クレートの候補一覧
実際のアプリの大半で役立ちます。必要に応じて追加してください。すべてが必須というわけではありません。
| クレート | 提供される機能 |
|---|---|
serde + serde_json | シリアライズ。#[derive(Serialize, Deserialize)] を付ければ完了です |
tokio | 非同期ランタイム。#[tokio::main]、tokio::spawn、非同期 I/O |
anyhow | アプリのエラー型。anyhow::Result<T>、bail!、context() |
thiserror | ライブラリのエラー列挙型。#[derive(thiserror::Error)] |
clap | CLI 引数の解析。#[derive(Parser)] を付ければ CLI を利用できます |
reqwest | HTTP クライアント。デフォルトで非同期で、ブロッキング機能も利用できます |
tracing + tracing-subscriber | 構造化ログ。非同期コードでは標準です(log を置き換えます) |
axum | Web フレームワーク。tokio + hyper + tower を基盤としています。2026年時点の標準です |
sqlx | データベースアクセス。非同期で、クエリはコンパイル時に検証されます。PostgreSQL、MySQL、SQLite に対応しています |
chrono | 日付と時刻。メンテナーは2026年1月にソフト非推奨を発表し、新しいコードでは jiff を推奨しています。jiff(BurntSushi)は後継ですが、2026年9月時点でもまだ 1.0 以前です(0.2.x で、1.0 に向けた課題は公開されていますが、時期は未定です)。現時点で serde/sqlx との統合を重視するなら chrono、1.0 以前の変更を許容できるなら jiff を選んでください |
それぞれの最小限の例については、references/crate-shortlist.md を参照してください。
避けるべき主なアンチパターン
新しく参加した人のコードレビューで必ず見かける間違いです。避けましょう。
- 構造体に
&str(またはあらゆる参照)を格納する。 呼び出し元すべてにライフタイム注釈が連鎖していきます。プロファイラーに裏付けられた理由がない限り、Stringを使いましょう。 - Python/JS のオブジェクトグラフを再現するために
Rc<RefCell<T>>(またはArc<Mutex<T>>)を使う。 まず、そもそも共有可変状態が必要か考えましょう。たいていは、通常の所有権を使うか、チャネル経由でデータを渡すほうがすっきりします。本当に必要な場合は、あとからスレッドを追加する際にリファクタリングせずに済むよう、Rc<RefCell<T>>よりArc<Mutex<T>>を優先しましょう。 - ライブラリの公開 API で
Box<dyn Error>を使う。 呼び出し元にダウンキャストを強いることになります。thiserrorで型付きエラー列挙型を定義しましょう。Box<dyn Error>はバイナリ内部で使うのは問題ありませんが、公開ライブラリでは使わないでください。 - プロトタイプやテスト以外で
.unwrap()と.expect()を使う。?を使って伝播させましょう。Optionの場合は、.ok_or_else(|| anyhow!(...))?で変換できます。値が本当に存在しないはずがない場合は、.unwrap()より.expect("why it cannot fail")を優先しましょう。メッセージが、不変条件を説明するコメントになります。 - コンパイルが通るまで力任せに
.clone()する。 クローンが正しい場合もありますが、借用チェッカーを黙らせるためにあちこちで.clone()を使っているなら、設計に問題があります。一歩下がって、誰が何を所有するのかについての3つの質問を考えましょう。 Derefを使って継承しようとする。Derefはスマートポインターのようなラッパーのためのもので、OOP 風の「継承」用ではありません。コンポジションを使いましょう。- すぐに
unsafeを使う。 アプリ開発者がこれを必要とすることは、基本的にありません。unsafeは借用チェッカーを無効にするものではありません。手動で不変条件を確認したという契約のもと、特定の5つの操作(生ポインターのデリファレンス、unsafe 関数の呼び出し、可変静的変数へのアクセス、unsafe トレイトの実装、共用体フィールドへのアクセス)を可能にするものです。 .lines()やread_lineで信頼できない入力を読む。 これらは上限なくメモリを確保します。BufRead::read_line自身のドキュメントにも、「攻撃者が改行も EOF も送らずに、バイトを送り続ける可能性がある」と警告されています。.lines()も内部では各項目でread_lineを呼び出すため、同じ挙動になります。いずれの場合も、悪意のある、または不正な形式のピアによってプロセスのメモリが枯渇する可能性があります。Read::take(n)で読み取り量に上限を設け、長すぎる行を破棄するにはBufRead::skip_until(1.83 から安定版)を使いましょう。for line in reader.lines()はどのチュートリアルでも最初に教えられますが、この問題に触れているものはほとんどありません。- 失ってはならないデータを
std::fs::writeに任せる。 この関数はファイルを切り詰めてからその場で書き込むため、書き込み中にクラッシュや停電が起きると、ファイルが空、または途中まで書き込まれた状態になります。また、正常に戻ったとしても、バイト列がディスクに書き込まれたことを意味するわけではありません。状態ファイルやマニフェストの場合は、同じディレクトリ内の一時ファイルに書き込み、File::sync_all()を呼び出してから、それを対象ファイルにrenameしましょう。Unix では、再起動後もリネームが保持されるよう、親ディレクトリでもsync_all()を呼び出します。(tempfile::NamedTempFile::persistが行うのはリネームであり、同期処理ではありません。)
後回しにすること
これらを初日から知っている必要はありません。必要になることがないものもあります。
- 構造体フィールドのライフタイム。 所有型を使えば避けられます。本当に必要になるときには、分かるはずです。
Pin、Futureの内部実装、手動のpoll実装。async fnと.awaitを使いましょう。unsafeと FFI。 アプリケーションコードで必要になることは、ほとんどありません。- 手続き型マクロ。 ライブラリ作者向けの領域です。
- 高階トレイト境界(
for<'a>)、分散、PhantomData。 エキスパート向けの領域です。 Cell、OnceCell、LazyLock、MaybeUninit。 明確な理由があるときに使いましょう。
最小限の Cargo.toml
単一クレート、エディション 2024、意見を反映した lint 設定です。新規プロジェクトにそのまま追加できます。
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
[dependencies]
[dev-dependencies]
[profile.release]
lto = "thin"
codegen-units = 1
# =============================================================================
# Lints. Loose-but-helpful: deny obvious bugs, warn on common smells, leave
# room to learn. Upgrade to clippy::pedantic later if you want the full ride.
# =============================================================================
[lints.rust]
# "deny", not "forbid": forbid cannot be lifted by a local #[allow] with a
# SAFETY comment, and that bites on ordinary mmap, not just FFI.
unsafe_code = "deny"
unreachable_pub = "warn"
[lints.clippy]
all = { level = "deny", priority = -1 }
# Idiomatic helpers
# Note: uninlined_format_args moved to clippy::pedantic (allow-by-default)
# during the 1.89/1.90 cycle, so an explicit warn keeps the nudge active.
uninlined_format_args = "warn"
semicolon_if_nothing_returned = "warn"
implicit_clone = "warn"
# Smells in non-prototype code
dbg_macro = "warn"
todo = "warn"
print_stdout = "warn" # use `tracing::info!` instead in real apps
lint を抑制する必要がある場合は、#[allow] よりも #[expect(clippy::lint_name, reason = "why")](1.81 から安定版)を優先してください。expect は lint が発生しなくなると警告するため、古い抑制が積み重なるのではなく、表面化します。Clippy の allow_attributes と allow_attributes_without_reason(どちらもオプトインの restriction グループ内)が、この方針を強制します。ただし、1 つ注意点があります。特定の OS でのみ発生する lint には #[cfg_attr(unix, expect(...))] が必要です。そうしないと、他の環境では expect が満たされず、警告が出ます。
rustfmt.toml
style_edition = "2024"
edition = "2024"
これで十分です。rustfmt のデフォルト設定は適切です。1 行に収まるコードを増やすために、use_small_heuristics = "Max" を追加するチームもあります。imports_granularity や group_imports のような高度なオプションは、2026 年 10 月時点ではまだ nightly 専用です(rustfmt の追跡 issue #4991 および #5083)。
編集を始める前に cargo fmt を実行してください(または、既存のフォーマットのずれがあれば、それだけを別途コミットしてください)。こうすることで、フォーマットの差分が変更内容に混ざらないようにできます。また、cargo fmt --check は独立した CI ステップにしてください。
rust-toolchain.toml(任意ですが推奨)
プロジェクトごとにツールチェーンを固定し、チーム全員が同じ Rust を使うようにします。ただし、正確なバージョンを指定した場合に限ります。channel = "stable" は「このマシンに最後にインストールされた stable を使う」という意味です。そのため、マシンごとに使用するバージョンが気付かないうちに異なり、正確な rustc バージョンをキーにするコンパイラーキャッシュも、マシン間でアーティファクトを共有できません。意図的にバージョンを更新してください(references/dev-environment.md を参照)。
[toolchain]
channel = "1.99.0"
components = ["rustfmt", "clippy", "rust-src"]
profile = "minimal"
このファイルは rustup の機能です。Nix、ディストリビューションのパッケージ、または Homebrew で提供された rustc はこのファイルを無視するため、そうしたマシンではインストール済みのコンパイラーが使われます。rustup を使っている場合でも、優先順位が最も高い設定ではありません。そのディレクトリに対する rustup override set(および RUSTUP_TOOLCHAIN、cargo +toolchain)がこの設定より優先されるため、設定を解除し忘れるとファイルがあっても機能しません。rustup show で、どのツールチェーンが有効か、またその理由を確認できます。rustup override unset で設定を解除できます。
.gitignore
/target
Cargo.lock をコミットしてください。cargo new はこのファイルを追跡対象にしており、Cargo ガイドも「迷ったら、Cargo.lock をバージョン管理システムにチェックインしてください」と勧めています。FAQ では、すべてのケースに当てはまるとはしておらず、「そうするかどうかは、パッケージのニーズによって異なります」としています。しかし、アプリケーションでは明らかに正しく、ライブラリでも今ではコミットするのが一般的なデフォルトです。
プロジェクト構成
my-app/
src/
main.rs # binary entry point: fn main()
lib.rs # OR a library crate root
config.rs # module: declared as `mod config;` in main.rs/lib.rs
api/ # nested module
mod.rs # OR `api.rs` next to api/ folder (2018+ style preferred)
users.rs
bin/ # extra binaries: src/bin/admin.rs -> `cargo run --bin admin`
examples/ # runnable examples: `cargo run --example demo`
tests/ # integration tests (each file is its own crate)
smoke.rs
Cargo.toml
Cargo.lock
rust-toolchain.toml
rustfmt.toml
.gitignore
ファイルが長くなるまでは mod { ... } を使ったインラインモジュールにし、その後でファイルに分割してください。最初から分割しないでください。
デフォルトでは、親モジュールから見た場合も含め、すべてが非公開です。 mod config; はモジュールを存在させるだけで、その中の項目にアクセスできるようにはしません。これは、初期によくある「なぜ呼び出せないのか」というエラーの原因です。修正するには、その項目に可視性キーワードを付けます(入れ子になっている場合は、モジュールにも付けます)。pub を付けると外部に公開され、pub(crate) を付けると自分のクレート内だけに公開されます。ライブラリの公開 API に含まれないものには、pub(crate) をデフォルトとして使うのが適切です。これによって、下の表にある unreachable_pub lint が有用な指摘をしてくれます。
上記の構成は 1 つのクレートです。共有ライブラリと CLI、サーバーとクライアントのように、2 つ目のクレートが必要になった時点で、Cargo ワークスペースが必要になります。また、単一クレートを超える成長に伴う Cargo.toml の設定、つまりフィーチャーフラグ、ビルドスクリプト、rust-version が実際に制御する内容も必要です。詳しくは references/project-shape.md を参照してください。
学習パス
- The Rust Book(https://doc.rust-lang.org/book/) - 標準的で、無料かつ最新の資料です。インタラクティブな Brown University 版(https://rust-book.cs.brown.edu/)では、クイズや視覚化が追加されています。
- Rustlings(https://github.com/rust-lang/rustlings) - The Book と並行して取り組める演習です。
- 100 Exercises to Learn Rust(https://rust-exercises.com/) - Rustlings の代わり、または補助教材として使える、やや新しい資料です。
- Rust for Rustaceans(Jon Gjengset) - 初心者向けの次に読む本です。慣れてから読みましょう。
- Zero to Production in Rust(Luca Palmieri) - バックエンドサービスを構築する場合におすすめです。注意:この本では
actix-webが使われていますが、2026 年の標準はaxumです。パターンはそのまま応用できます。
構文を調べるには、Rust by Example(https://doc.rust-lang.org/rust-by-example/)をご覧ください。
厳選されたクレートのおすすめについては、blessed.rs(https://blessed.rs/crates)をご覧ください。
リファレンスドキュメント
詳しい内容は references/ にあります。各トピックに取り組む際に、該当するドキュメントをお読みください。
- ownership-and-types.md - 所有権、借用、ライフタイム、
String/&str/Cow、文字列にインデックスを付けられない理由、スライス、HashMapとentryAPI、スマートポインター、自己参照構造体の落とし穴 - error-handling.md -
Result、?、anyhowとthiserrorのパターン、境界でラップし、その前ではラップしない方法、カスタムエラー enum、panic!が適切な場合 - traits-and-generics.md - 境界としてのトレイト、
dynとimpl Traitとジェネリクスの違い、よく使われる derive、From/Into/Display/Debug、クロージャとFn/FnMut/FnOnceファミリー、包括的実装、孤児ルール - async-basics.md - 非同期処理の前にスレッドと
mpscを学ぶこと、tokio、#[tokio::main]、.await、Send/Sync、同時実行数の制限、SIGTERM 時のキャンセルとグレースフルシャットダウン、よくある落とし穴(非同期処理内でのブロッキング、.awaitをまたぐMutexGuard) - crate-shortlist.md - 前述の 8 つのクレートそれぞれの最小限の使用例
- project-shape.md - 単一クレートを超えたプロジェクト構成:ワークスペースと継承される依存関係、
[workspace.lints]、フィーチャーフラグ、build.rs、rust-versionが制御する内容、#[non_exhaustive] - testing.md - テストするものと省略するもの、実用的なテスト構成、テストスイートを高速に保つ方法、最小限で効果の高いツールキット
- dev-environment.md - 高速なビルドループ、ビルドキャッシュ(kache の設定とその注意点、sccache、rust-cache)、
target/の肥大化を防ぐ方法、プラットフォームに応じたリンカーのガイダンス、CI、ファイルウォッチャー - releasing.md - バイナリのリリース:
distと、手動で構築するrelease-plz+cargo-zigbuildパイプラインの比較、[profile.dist]、1 つのランナーからすべてのターゲット向けにクロスコンパイルする方法、binstall/Homebrew/Nix への配布、公開するクレートに含まれる内容を制御する方法 - performance.md - 最適化の前にプロファイリングすること、criterion/divan によるベンチマーク、実際に効果のある実行時の改善