到目前為止程式碼都擠在 src/main.rs,真實專案不會這樣寫。這篇把 Rust 怎麼切多檔案、怎麼控制可見性、怎麼引用第三方 crate 一次串完。掌握這些,下一篇再加上 trait,組織 Rust 程式的整套工具就齊了。
名詞先釐清
四個層次,從大到小:
| 概念 | 說明 |
|---|---|
| Workspace | 一組 packages,共用同一個 Cargo.lock 與 target/ |
| Package | 一個 Cargo.toml 管理的單位,可包含一個 library crate + 多個 binary crate |
| Crate | 編譯單位,產出一個 library 或一個 binary |
| Module | 在 crate 內組織程式碼的單位,控制可見性與命名空間 |
Workspace
└── Package(Cargo.toml)
├── Crate(lib.rs,library)
│ ├── Module(mod foo)
│ └── Module(mod bar)
└── Crate(main.rs,binary)
Crate root
每個 crate 有一個 root file:
- Library crate:
src/lib.rs - Binary crate:
src/main.rs - 額外 binaries:
src/bin/<name>.rs
cargo new 預設建立 binary crate(產生 src/main.rs)。
cargo new --lib name 建立 library crate(產生 src/lib.rs)。
mod:宣告模組
模組是 crate 內的命名空間。在 src/main.rs 或 src/lib.rs 寫 mod foo;,編譯器會去找:
src/foo.rs(單一檔)src/foo/mod.rs(資料夾風格,舊式)src/foo.rs配src/foo/子資料夾(新式,2018 edition 之後)
Rust 2018 edition 起優先用 src/foo.rs(平行檔案)。src/foo/mod.rs 是舊式寫法——子模組多時目錄結構清楚,但檔名全叫 mod.rs,在編輯器分頁裡容易混淆,新程式碼建議避免。
範例專案結構:
src/
├── main.rs
├── garden.rs
└── garden/
└── vegetables.rs
src/main.rs:
mod garden; // 載入 src/garden.rs
fn main() {
let p = garden::vegetables::Asparagus {};
println!("planted!");
}
src/garden.rs:
pub mod vegetables; // 載入 src/garden/vegetables.rs
src/garden/vegetables.rs:
pub struct Asparagus {}
也可以直接 inline 寫在同一檔:
mod garden {
pub mod vegetables {
pub struct Asparagus {}
}
}
pub:可見性
預設所有東西都是私有的——模組、函式、struct、struct 欄位、enum 變體通通是。
mod kitchen {
pub fn cook() { // pub:外面可以叫
prepare();
}
fn prepare() { } // 私有:只有 kitchen 內部能叫
}
fn main() {
kitchen::cook(); // ✅
// kitchen::prepare(); // ❌ private function
}
pub 還有更細的控制:
pub fn foo() {} // 對所有人公開
pub(crate) fn bar() {} // 整個 crate 內可見,但不對外
pub(super) fn baz() {} // 父模組可見
pub(in crate::path) fn x() {} // 限定路徑可見
struct 與 enum 的 pub 差異
struct:把 struct 標 pub 不會自動把欄位變 pub,欄位要一個一個標。
mod food {
pub struct Pizza {
pub topping: String, // 公開欄位
size: u32, // 私有,外面拿不到
}
impl Pizza {
pub fn new(topping: String) -> Self {
Pizza { topping, size: 12 }
}
}
}
fn main() {
let p = food::Pizza::new(String::from("cheese"));
println!("{}", p.topping);
// println!("{}", p.size); // ❌
}
enum:標 pub 後所有變體自動 pub,因為大家用 enum 通常是要 match 所有 case。
設計理由:match 一個 enum 必須窮舉所有變體。如果某個變體對外隱藏,使用方永遠寫不出完整的 match——除非用 _ 兜底,但那就失去型別安全的好處了。所以 struct 欄位可以各自隱藏,enum 變體不行。
mod color {
pub enum Light {
Red,
Yellow,
Green,
}
}
路徑:絕對 vs 相對
// 假設這是 src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 絕對路徑:從 crate root 開始
crate::front_of_house::hosting::add_to_waitlist();
// 相對路徑:從目前模組開始
front_of_house::hosting::add_to_waitlist();
}
super = 上一層模組,self = 目前模組:
mod parent {
fn helper() {}
mod child {
fn use_helper() {
super::helper(); // 找上一層的 helper
}
}
}
use:把長路徑帶到當前 scope
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist(); // 直接寫 hosting:: 即可
}
慣例:函式的 use 到模組那層就停(use ...::hosting),呼叫時帶模組名(hosting::add_to_waitlist),讀程式時才看得出函式來源。
例外:型別、struct、enum、trait 慣例 use 到本身:
use std::collections::HashMap;
use std::io::Read;
let m = HashMap::new();
as:避免衝突
use std::fmt::Result;
use std::io::Result as IoResult;
fn f1() -> Result { Ok(()) }
fn f2() -> IoResult<()> { Ok(()) }
pub use:re-export
把內部模組的東西轉手公開到外層:
mod inner {
pub fn helper() {}
}
pub use inner::helper;
// 外面可以直接呼叫 crate::helper(),不用知道它住在 inner
很多 library 用這招把 API 集中在 lib.rs 暴露出來。
多重 import
use std::io::{self, Read, Write};
// 等同:
// use std::io;
// use std::io::Read;
// use std::io::Write;
use std::collections::*; // glob,會把 collections 下所有 pub 的東西帶進來
* 慎用,會污染命名空間,通常只在 prelude / 測試模組使用。
外部依賴:使用第三方 crate
Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
rand = "0.8"
或用指令:
cargo add serde --features derive
cargo add rand
程式裡:
use rand::Rng;
fn main() {
let n: u8 = rand::thread_rng().gen_range(1..=100);
println!("{n}");
}
std 與 prelude
std 是標準庫。最常用的那批東西(Option、Result、Vec、String、println!、Box、Some、None、Ok、Err…)放在 prelude,自動 import 到每個檔案,不用 use。
prelude 之外的就要手動 use:
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
一個合理的 library crate 結構
my_lib/
├── Cargo.toml
└── src/
├── lib.rs // 對外 API(用 pub use 集中暴露)
├── error.rs // 錯誤型別
├── client.rs // HTTP client
├── client/
│ ├── auth.rs
│ └── retry.rs
└── models/
├── mod.rs // 或 src/models.rs
├── user.rs
└── post.rs
src/lib.rs:
mod error;
mod client;
mod models;
// 對外暴露的 API 集中在這
pub use error::{Error, Result};
pub use client::Client;
pub use models::{User, Post};
呼叫方:
use my_lib::{Client, User, Result};
binary + library 混合 package
Cargo.toml 裡 package 名是 my_app:
src/
├── lib.rs // library crate(package 同名 my_app)
├── main.rs // binary crate
└── bin/
└── tool.rs // 額外 binary:my_app-tool
src/main.rs 用 library 的東西:
use my_app::{Client, Config}; // 要用 package 名而不是 crate
fn main() {
let c = Client::new();
// ...
}
跑時:
cargo run # 跑 src/main.rs
cargo run --bin tool # 跑 src/bin/tool.rs
常見錯誤訊息對照
| 訊息 | 意思 |
|---|---|
function ... is private | 缺 pub |
unresolved import ... | use 路徑寫錯 / 模組沒宣告 |
cannot find ... in this scope | 沒 use 進來 / 不在這個模組可見 |
module ... does not exist | mod foo; 但找不到 src/foo.rs |
模組系統打通後,最後一篇 trait 把「行為」這個維度補進來——也就是 Rust 怎麼做多型、抽象與泛型約束。
Latest Updates
- 2026.06.11 Content updated
