SC Day 28
Rust clap 命令行解析 + 项目结构化(mod/lib/bin) + CLI v2
### 一、clap — 命令行参数解析
2026-04-28
第二阶段:框架实战 (Day 25-30)RustclapCLI模块系统项目结构
日期: 2026-04-28 方向: Rust 阶段: 第二阶段:框架实战 (Day 25-30) 标签: #Rust #clap #CLI #模块系统 #项目结构
今日目标
- 掌握 clap 的 derive API(Parser, Subcommand, Args)
- 深入理解 Rust 模块系统(mod, pub, use, lib.rs vs main.rs)
- 实现多子命令 CLI 工具(balance/tx/token)
- 学会将项目结构化为可维护的模块
核心概念
一、clap — 命令行参数解析
clap 是 Rust 生态中最主流的命令行解析库。v4 版本推荐使用 derive API,通过派生宏自动生成解析代码。
1.1 基础用法
use clap::Parser;
/// 区块链查询 CLI 工具
#[derive(Parser, Debug)]
#[command(name = "chain-cli")]
#[command(version = "0.2.0")]
#[command(about = "Query blockchain data from the command line")]
#[command(author = "Momo")]
struct Cli {
/// 以太坊地址
#[arg(short, long)]
address: String,
/// 链名称 (ethereum, arbitrum, polygon)
#[arg(short, long, default_value = "ethereum")]
chain: String,
/// 输出格式
#[arg(short, long, default_value = "text", value_parser = ["text", "json", "csv"])]
format: String,
/// 详细输出
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
fn main() {
let cli = Cli::parse();
println!("Address: {}", cli.address);
println!("Chain: {}", cli.chain);
println!("Format: {}", cli.format);
println!("Verbose level: {}", cli.verbose);
}
使用方式:
# 基本用法
chain-cli --address 0x1234... --chain arbitrum
# 简写
chain-cli -a 0x1234... -c polygon -f json
# 多个 -v 增加详细度
chain-cli -a 0x1234... -vvv # verbose = 3
# 帮助
chain-cli --help
chain-cli --version
1.2 子命令 (Subcommands)
这是 CLI 工具的核心模式 — 不同功能用不同子命令:
use clap::{Parser, Subcommand, Args};
#[derive(Parser, Debug)]
#[command(name = "chain-cli", version, about)]
struct Cli {
/// 全局选项: RPC URL
#[arg(long, env = "ETH_RPC_URL", global = true)]
rpc_url: Option<String>,
/// 全局选项: API Key
#[arg(long, env = "ETHERSCAN_API_KEY", global = true)]
api_key: Option<String>,
/// 全局选项: 详细输出
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// 查询账户余额
Balance(BalanceArgs),
/// 查询交易历史
Tx(TxArgs),
/// 查询 Token 信息
Token(TokenArgs),
/// 查询当前 Gas 价格
Gas,
}
#[derive(Args, Debug)]
struct BalanceArgs {
/// 以太坊地址
#[arg(short, long)]
address: String,
/// 链 (ethereum, arbitrum, polygon, base, optimism)
#[arg(short, long, default_value = "ethereum")]
chain: String,
/// 同时查询所有支持的链
#[arg(long)]
all_chains: bool,
}
#[derive(Args, Debug)]
struct TxArgs {
/// 以太坊地址
#[arg(short, long)]
address: String,
/// 显示最近 N 笔交易
#[arg(short, long, default_value = "10")]
count: u32,
/// 只显示发出的交易
#[arg(long)]
outgoing_only: bool,
/// 只显示收到的交易
#[arg(long)]
incoming_only: bool,
}
#[derive(Args, Debug)]
struct TokenArgs {
/// Token 合约地址
#[arg(short = 'c', long)]
contract: String,
/// 查询某地址的 Token 余额 (可选)
#[arg(short, long)]
holder: Option<String>,
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Commands::Balance(args) => {
println!("Querying balance for {} on {}", args.address, args.chain);
if args.all_chains {
println!("Checking all chains...");
}
}
Commands::Tx(args) => {
println!("Querying {} transactions for {}", args.count, args.address);
}
Commands::Token(args) => {
println!("Token contract: {}", args.contract);
if let Some(holder) = &args.holder {
println!("Checking balance for holder: {}", holder);
}
}
Commands::Gas => {
println!("Querying current gas prices...");
}
}
}
使用方式:
chain-cli balance --address 0x1234... --chain arbitrum
chain-cli balance -a 0x1234... --all-chains
chain-cli tx -a 0x1234... --count 20 --outgoing-only
chain-cli token -c 0xdAC17F... --holder 0x1234...
chain-cli gas
chain-cli --rpc-url https://... balance -a 0x1234...
1.3 高级 clap 特性
use clap::{Parser, ValueEnum};
// 用枚举限制选项值
#[derive(Debug, Clone, ValueEnum)]
enum Chain {
Ethereum,
Arbitrum,
Optimism,
Base,
Polygon,
}
impl std::fmt::Display for Chain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Chain::Ethereum => write!(f, "ethereum"),
Chain::Arbitrum => write!(f, "arbitrum"),
Chain::Optimism => write!(f, "optimism"),
Chain::Base => write!(f, "base"),
Chain::Polygon => write!(f, "polygon"),
}
}
}
#[derive(Debug, Clone, ValueEnum)]
enum OutputFormat {
Text,
Json,
Csv,
Table,
}
#[derive(Parser, Debug)]
struct QueryArgs {
/// 查询链
#[arg(short, long, value_enum, default_value_t = Chain::Ethereum)]
chain: Chain,
/// 输出格式
#[arg(short, long, value_enum, default_value_t = OutputFormat::Table)]
format: OutputFormat,
/// 自定义 RPC URL (覆盖默认值)
#[arg(long, env = "ETH_RPC_URL")]
rpc_url: Option<String>,
/// 请求超时 (秒)
#[arg(long, default_value = "10", value_parser = clap::value_parser!(u64).range(1..=60))]
timeout: u64,
}
二、Rust 模块系统
2.1 模块基础
Rust 的模块系统用于组织代码,控制可见性(封装),避免命名冲突。
模块系统核心规则:
1. 每个 .rs 文件就是一个模块
2. mod 声明引入子模块
3. pub 控制可见性
4. use 引入路径简写
5. crate:: 指代当前 crate 根
6. self:: 指代当前模块
7. super:: 指代父模块
2.2 文件即模块
src/
├── main.rs # 二进制入口 (bin crate root)
├── lib.rs # 库入口 (lib crate root)
├── config.rs # config 模块
├── client.rs # client 模块
├── commands/ # commands 模块 (目录形式)
│ ├── mod.rs # commands 模块入口
│ ├── balance.rs # commands::balance 子模块
│ ├── tx.rs # commands::tx 子模块
│ └── token.rs # commands::token 子模块
└── types/ # types 模块
├── mod.rs
└── chain.rs
2.3 lib.rs vs main.rs
| 文件 | 角色 | 用途 |
|---|---|---|
lib.rs | 库 crate 根 | 定义公共 API,可被其他 crate 依赖 |
main.rs | 二进制 crate 根 | 程序入口点,通常调用 lib.rs 的函数 |
最佳实践: 将逻辑放在 lib.rs,main.rs 只做参数解析和调用。这样可以:
- 方便编写集成测试(只能测试 lib crate 的公共 API)
- 方便被其他项目复用
- 保持关注点分离
2.4 可见性规则
// 默认: 私有 (只在当前模块可见)
fn private_fn() {}
// pub: 公共 (任何地方可见)
pub fn public_fn() {}
// pub(crate): 包内可见 (当前 crate 内可见)
pub(crate) fn crate_visible_fn() {}
// pub(super): 父模块可见
pub(super) fn parent_visible_fn() {}
// pub(in path): 指定路径可见
pub(in crate::commands) fn commands_visible_fn() {}
// 结构体的字段也可以独立控制可见性
pub struct Config {
pub chain: String, // 公开
pub(crate) api_key: String, // 包内可见
secret: String, // 私有
}
代码实战
完整 CLI v2 项目
项目结构
chain-cli/
├── Cargo.toml
├── .env.example
├── src/
│ ├── main.rs # 入口 + clap 解析
│ ├── lib.rs # 库导出
│ ├── config.rs # 配置管理
│ ├── client.rs # HTTP/RPC 客户端
│ ├── types.rs # 公共类型定义
│ ├── utils.rs # 工具函数
│ └── commands/ # 子命令实现
│ ├── mod.rs # 模块入口
│ ├── balance.rs # balance 子命令
│ ├── tx.rs # tx 子命令
│ └── token.rs # token 子命令
Cargo.toml
[package]
name = "chain-cli"
version = "0.2.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dotenv = "0.15"
src/types.rs
use clap::ValueEnum;
use serde::Deserialize;
use std::fmt;
/// 支持的链
#[derive(Debug, Clone, ValueEnum)]
pub enum Chain {
Ethereum,
Arbitrum,
Optimism,
Base,
Polygon,
}
impl Chain {
pub fn rpc_url(&self) -> &'static str {
match self {
Chain::Ethereum => "https://eth.llamarpc.com",
Chain::Arbitrum => "https://arb1.arbitrum.io/rpc",
Chain::Optimism => "https://mainnet.optimism.io",
Chain::Base => "https://mainnet.base.org",
Chain::Polygon => "https://polygon-rpc.com",
}
}
pub fn chain_id(&self) -> u64 {
match self {
Chain::Ethereum => 1,
Chain::Arbitrum => 42161,
Chain::Optimism => 10,
Chain::Base => 8453,
Chain::Polygon => 137,
}
}
pub fn explorer_url(&self) -> &'static str {
match self {
Chain::Ethereum => "https://etherscan.io",
Chain::Arbitrum => "https://arbiscan.io",
Chain::Optimism => "https://optimistic.etherscan.io",
Chain::Base => "https://basescan.org",
Chain::Polygon => "https://polygonscan.com",
}
}
pub fn all() -> Vec<Chain> {
vec![
Chain::Ethereum,
Chain::Arbitrum,
Chain::Optimism,
Chain::Base,
Chain::Polygon,
]
}
}
impl fmt::Display for Chain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Chain::Ethereum => write!(f, "Ethereum"),
Chain::Arbitrum => write!(f, "Arbitrum"),
Chain::Optimism => write!(f, "Optimism"),
Chain::Base => write!(f, "Base"),
Chain::Polygon => write!(f, "Polygon"),
}
}
}
/// RPC 响应
#[derive(Deserialize)]
pub struct RpcResponse {
pub jsonrpc: String,
pub id: u64,
pub result: Option<serde_json::Value>,
pub error: Option<RpcError>,
}
#[derive(Deserialize, Debug)]
pub struct RpcError {
pub code: i64,
pub message: String,
}
/// 余额查询结果
#[derive(Debug)]
pub struct BalanceResult {
pub chain: String,
pub address: String,
pub balance_wei: u128,
pub balance_eth: f64,
pub latency_ms: u128,
}
/// Etherscan API 响应
#[derive(Deserialize)]
pub struct EtherscanResponse<T> {
pub status: String,
pub message: String,
pub result: T,
}
/// 交易信息
#[derive(Debug, Deserialize)]
pub struct TransactionInfo {
#[serde(rename = "blockNumber")]
pub block_number: String,
#[serde(rename = "timeStamp")]
pub timestamp: String,
pub hash: String,
pub from: String,
pub to: String,
pub value: String,
#[serde(rename = "gasUsed")]
pub gas_used: String,
#[serde(rename = "isError")]
pub is_error: String,
#[serde(rename = "functionName")]
pub function_name: String,
}
src/config.rs
use crate::types::Chain;
use std::env;
/// 应用配置
pub struct AppConfig {
pub etherscan_api_key: String,
pub default_rpc_url: String,
pub timeout_secs: u64,
}
impl AppConfig {
pub fn from_env() -> Self {
Self {
etherscan_api_key: env::var("ETHERSCAN_API_KEY")
.unwrap_or_else(|_| "YourApiKeyToken".to_string()),
default_rpc_url: env::var("ETH_RPC_URL")
.unwrap_or_else(|_| Chain::Ethereum.rpc_url().to_string()),
timeout_secs: env::var("TIMEOUT_SECS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(10),
}
}
}
src/client.rs
use crate::types::*;
use reqwest::Client;
use serde::Serialize;
use std::time::{Duration, Instant};
#[derive(Serialize)]
struct RpcRequest {
jsonrpc: String,
method: String,
params: Vec<serde_json::Value>,
id: u64,
}
/// 区块链 RPC 客户端
pub struct ChainClient {
http: Client,
pub etherscan_key: String,
}
impl ChainClient {
pub fn new(timeout_secs: u64, etherscan_key: String) -> Self {
let http = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.expect("Failed to create HTTP client");
Self { http, etherscan_key }
}
/// 通过 RPC 查询余额
pub async fn get_balance(
&self,
rpc_url: &str,
address: &str,
) -> Result<BalanceResult, Box<dyn std::error::Error + Send + Sync>> {
let start = Instant::now();
let request = RpcRequest {
jsonrpc: "2.0".to_string(),
method: "eth_getBalance".to_string(),
params: vec![
serde_json::Value::String(address.to_string()),
serde_json::Value::String("latest".to_string()),
],
id: 1,
};
let resp: RpcResponse = self.http.post(rpc_url).json(&request).send().await?.json().await?;
let latency = start.elapsed().as_millis();
if let Some(err) = resp.error {
return Err(format!("RPC error: {}", err.message).into());
}
let hex = resp.result.ok_or("No result")?.as_str().ok_or("Not string")?.to_string();
let balance_wei = u128::from_str_radix(hex.trim_start_matches("0x"), 16)?;
let balance_eth = balance_wei as f64 / 1e18;
Ok(BalanceResult {
chain: String::new(),
address: address.to_string(),
balance_wei,
balance_eth,
latency_ms: latency,
})
}
/// 通过 RPC 查询区块号
pub async fn get_block_number(
&self,
rpc_url: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let request = RpcRequest {
jsonrpc: "2.0".to_string(),
method: "eth_blockNumber".to_string(),
params: vec![],
id: 1,
};
let resp: RpcResponse = self.http.post(rpc_url).json(&request).send().await?.json().await?;
if let Some(err) = resp.error {
return Err(format!("RPC error: {}", err.message).into());
}
let hex = resp.result.ok_or("No result")?.as_str().ok_or("Not string")?.to_string();
Ok(u64::from_str_radix(hex.trim_start_matches("0x"), 16)?)
}
/// 查询交易列表 (Etherscan)
pub async fn get_transactions(
&self,
address: &str,
count: u32,
) -> Result<Vec<TransactionInfo>, Box<dyn std::error::Error + Send + Sync>> {
let url = format!(
"https://api.etherscan.io/api?module=account&action=txlist&address={}&startblock=0&endblock=99999999&page=1&offset={}&sort=desc&apikey={}",
address, count, self.etherscan_key
);
let resp: EtherscanResponse<Vec<TransactionInfo>> =
self.http.get(&url).send().await?.json().await?;
if resp.status != "1" {
return Err(format!("Etherscan error: {}", resp.message).into());
}
Ok(resp.result)
}
}
src/utils.rs
/// 格式化 ETH 余额
pub fn format_eth(eth: f64) -> String {
if eth >= 1000.0 {
format!("{:.2} ETH", eth)
} else if eth >= 1.0 {
format!("{:.4} ETH", eth)
} else if eth >= 0.001 {
format!("{:.6} ETH", eth)
} else {
format!("{:.10} ETH", eth)
}
}
/// 缩短地址显示
pub fn shorten_address(addr: &str) -> String {
if addr.len() > 10 {
format!("{}...{}", &addr[..6], &addr[addr.len() - 4..])
} else {
addr.to_string()
}
}
/// 验证以太坊地址格式
pub fn validate_address(addr: &str) -> Result<(), String> {
if !addr.starts_with("0x") {
return Err("Address must start with 0x".to_string());
}
if addr.len() != 42 {
return Err("Address must be 42 characters (0x + 40 hex)".to_string());
}
if !addr[2..].chars().all(|c| c.is_ascii_hexdigit()) {
return Err("Address contains non-hex characters".to_string());
}
Ok(())
}
/// 格式化时间戳
pub fn format_timestamp(ts: &str) -> String {
let secs: i64 = ts.parse().unwrap_or(0);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let diff = now - secs;
if diff < 60 {
format!("{}s ago", diff)
} else if diff < 3600 {
format!("{}m ago", diff / 60)
} else if diff < 86400 {
format!("{}h ago", diff / 3600)
} else {
format!("{}d ago", diff / 86400)
}
}
src/commands/mod.rs
pub mod balance;
pub mod tx;
pub mod token;
src/commands/balance.rs
use crate::client::ChainClient;
use crate::types::Chain;
use crate::utils::{format_eth, shorten_address, validate_address};
pub async fn execute(client: &ChainClient, address: &str, chain: &Chain, all_chains: bool) {
if let Err(e) = validate_address(address) {
eprintln!("Invalid address: {}", e);
return;
}
if all_chains {
execute_all_chains(client, address).await;
} else {
execute_single_chain(client, address, chain).await;
}
}
async fn execute_single_chain(client: &ChainClient, address: &str, chain: &Chain) {
println!("Querying balance on {}...", chain);
match client.get_balance(chain.rpc_url(), address).await {
Ok(result) => {
println!(
"Address: {}\nChain: {}\nBalance: {}\nLatency: {}ms",
shorten_address(address),
chain,
format_eth(result.balance_eth),
result.latency_ms
);
}
Err(e) => eprintln!("Error: {}", e),
}
}
async fn execute_all_chains(client: &ChainClient, address: &str) {
println!("Querying all chains for {}...\n", shorten_address(address));
let chains = Chain::all();
let mut handles = Vec::new();
for chain in &chains {
let rpc_url = chain.rpc_url().to_string();
let addr = address.to_string();
let chain_name = chain.to_string();
let client_clone = ChainClient::new(10, client.etherscan_key.clone());
handles.push(tokio::spawn(async move {
let result = client_clone.get_balance(&rpc_url, &addr).await;
(chain_name, result)
}));
}
println!("{:<12} {:>15} {:>10}", "Chain", "Balance", "Latency");
println!("{}", "-".repeat(40));
let mut total = 0.0;
for handle in handles {
if let Ok((chain_name, result)) = handle.await {
match result {
Ok(r) => {
println!(
"{:<12} {:>15} {:>8}ms",
chain_name,
format_eth(r.balance_eth),
r.latency_ms
);
total += r.balance_eth;
}
Err(e) => {
println!("{:<12} {:>15} {:>10}", chain_name, "Error", e);
}
}
}
}
println!("{}", "-".repeat(40));
println!("{:<12} {:>15}", "Total", format_eth(total));
}
src/commands/tx.rs
use crate::client::ChainClient;
use crate::utils::{format_eth, format_timestamp, shorten_address, validate_address};
pub async fn execute(
client: &ChainClient,
address: &str,
count: u32,
outgoing_only: bool,
incoming_only: bool,
) {
if let Err(e) = validate_address(address) {
eprintln!("Invalid address: {}", e);
return;
}
println!(
"Recent transactions for {} (last {}):\n",
shorten_address(address),
count
);
match client.get_transactions(address, count).await {
Ok(txs) => {
let filtered: Vec<_> = txs
.iter()
.filter(|tx| {
if outgoing_only {
tx.from.to_lowercase() == address.to_lowercase()
} else if incoming_only {
tx.to.to_lowercase() == address.to_lowercase()
} else {
true
}
})
.collect();
if filtered.is_empty() {
println!(" No transactions found.");
return;
}
for (i, tx) in filtered.iter().enumerate() {
let value_eth: f64 = tx.value.parse::<f64>().unwrap_or(0.0) / 1e18;
let direction = if tx.from.to_lowercase() == address.to_lowercase() {
"OUT"
} else {
"IN "
};
let status = if tx.is_error == "0" { "OK" } else { "FAIL" };
let method = if tx.function_name.is_empty() {
"Transfer".to_string()
} else {
tx.function_name.split('(').next().unwrap_or("Unknown").to_string()
};
println!(
" {}. [{}][{}] {} | {} | {} | {}",
i + 1,
direction,
status,
format_eth(value_eth),
method,
shorten_address(&tx.hash),
format_timestamp(&tx.timestamp),
);
}
}
Err(e) => eprintln!("Error: {}", e),
}
}
src/commands/token.rs
use crate::utils::validate_address;
pub async fn execute(contract: &str, holder: &Option<String>) {
if let Err(e) = validate_address(contract) {
eprintln!("Invalid contract address: {}", e);
return;
}
println!("Token info for contract: {}", contract);
if let Some(holder_addr) = holder {
if let Err(e) = validate_address(holder_addr) {
eprintln!("Invalid holder address: {}", e);
return;
}
println!("Checking balance for holder: {}", holder_addr);
}
// TODO: 实现 ERC20 合约查询
println!("(Token query will be implemented in CLI v3)");
}
src/lib.rs
pub mod client;
pub mod commands;
pub mod config;
pub mod types;
pub mod utils;
src/main.rs
use clap::{Parser, Subcommand, Args};
use chain_cli::client::ChainClient;
use chain_cli::config::AppConfig;
use chain_cli::types::Chain;
use chain_cli::commands;
#[derive(Parser)]
#[command(name = "chain-cli", version = "0.2.0")]
#[command(about = "Blockchain query CLI tool")]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Query account balance
Balance(BalanceArgs),
/// Query transaction history
Tx(TxArgs),
/// Query token information
Token(TokenArgs),
}
#[derive(Args)]
struct BalanceArgs {
#[arg(short, long)]
address: String,
#[arg(short, long, value_enum, default_value_t = Chain::Ethereum)]
chain: Chain,
#[arg(long)]
all_chains: bool,
}
#[derive(Args)]
struct TxArgs {
#[arg(short, long)]
address: String,
#[arg(short, long, default_value = "10")]
count: u32,
#[arg(long)]
outgoing_only: bool,
#[arg(long)]
incoming_only: bool,
}
#[derive(Args)]
struct TokenArgs {
#[arg(short = 'c', long)]
contract: String,
#[arg(short, long)]
holder: Option<String>,
}
#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
let cli = Cli::parse();
let config = AppConfig::from_env();
let client = ChainClient::new(config.timeout_secs, config.etherscan_api_key);
match cli.command {
Commands::Balance(args) => {
commands::balance::execute(&client, &args.address, &args.chain, args.all_chains).await;
}
Commands::Tx(args) => {
commands::tx::execute(
&client,
&args.address,
args.count,
args.outgoing_only,
args.incoming_only,
).await;
}
Commands::Token(args) => {
commands::token::execute(&args.contract, &args.holder).await;
}
}
}
关键要点总结
模块系统心智模型
crate (lib.rs 或 main.rs)
├── mod config; → config.rs
├── mod types; → types.rs
├── mod client; → client.rs
├── mod utils; → utils.rs
└── mod commands; → commands/mod.rs
├── mod balance; → commands/balance.rs
├── mod tx; → commands/tx.rs
└── mod token; → commands/token.rs
clap 核心概念
| 概念 | 宏/属性 | 说明 |
|---|---|---|
| Parser | #[derive(Parser)] | CLI 根结构 |
| Subcommand | #[derive(Subcommand)] | 子命令枚举 |
| Args | #[derive(Args)] | 参数组 |
| ValueEnum | #[derive(ValueEnum)] | 枚举选项 |
#[arg(short, long)] | 短/长选项 | -a / --address |
#[arg(env = "...")] | 环境变量 | 可从环境变量读取 |
#[arg(global = true)] | 全局选项 | 所有子命令共享 |
常见误区
- 模块声明遗漏: 每个文件都需要在父模块的
mod.rs或父文件中用mod xxx;声明 - pub 遗漏: 跨模块调用的函数、结构体、字段都需要
pub - clap 的 env 属性需要 feature:
clap = { features = ["env"] } - 子命令名称大小写: clap 自动将 PascalCase 转为 kebab-case(
AllChains->all-chains) - main.rs 引用 lib.rs: 使用
use crate_name::module,不是use crate::module
面试关联
| 面试题 | 本课关联 |
|---|---|
| "如何组织大型 Rust 项目?" | lib.rs + mod + 目录结构 |
| "Rust 的可见性规则?" | pub / pub(crate) / 默认私有 |
| "如何设计 CLI 工具?" | clap + 子命令 + 全局选项 |
| "lib crate 和 bin crate 的区别?" | lib.rs 是库,main.rs 是二进制 |