Rust 工程实践记录:可靠性、并发和可维护边界
Rust 给人的感觉很特别。它不像 Python 那样轻快,也不像 C++ 那样把大量自由交给工程师自己约束。Rust 更像一个会不断追问你的同事:这个值到底归谁,失败怎么处理,引用能活多久,线程之间能不能安全传递。刚开始写时,这些问题会让人觉得束手束脚;写久一点会发现,它其实是在逼你把很多本来模糊的工程边界说清楚。
我喜欢 Rust 的地方,不是它能替代所有语言,也不是它让程序永远没有 bug。它的价值在于把一部分可靠性前移。内存安全、数据竞争、错误传播、依赖组织、格式化和测试工具,这些东西在 Rust 里有比较统一的默认路径。你仍然要做设计,仍然会写出糟糕抽象,仍然可能把异步系统写复杂,但语言和工具链会帮你拦住不少低级问题。
这篇记录不想把 Rust 写成信仰文章。Rust 有明显成本:学习曲线高,编译器严格,生命周期和 trait 设计需要经验,异步生态也有自己的复杂度。真正写工程时,应该同时看到它的可靠性收益和维护成本。适合 Rust 的地方,要把它用好;不适合的地方,也没必要硬上。
所有权把模糊关系变成显式关系
Rust 最核心的变化,是所有权。很多语言里,对象被谁持有、什么时候释放、谁可以修改,往往靠约定和运行时行为。Rust 把这些关系放到编译期检查。刚开始会觉得麻烦,因为很多以前随手写的代码现在过不了编译;但这也迫使你把资源关系理清楚。
#[derive(Debug)]struct Buffer { bytes: Vec<u8>,}
fn consume(buffer: Buffer) -> usize { buffer.bytes.len()}
fn main() { let buffer = Buffer { bytes: vec![1, 2, 3] }; let size = consume(buffer); println!("{size}");}consume 拿走了 Buffer,调用后原来的 buffer 不能再用。这看起来严格,但它表达得很清楚:所有权已经转移。如果函数只是读取,就应该借用。
fn checksum(bytes: &[u8]) -> u32 { bytes.iter().fold(0u32, |acc, item| acc.wrapping_add(*item as u32))}
fn main() { let data = vec![1, 2, 3, 4]; let sum = checksum(&data); println!("{sum}"); println!("{}", data.len());}这里 checksum 不拥有数据,只读借用。调用之后 data 仍然能继续使用。函数签名已经说明了关系,不需要额外文档解释。
所有权的好处在小例子里不算明显,在复杂系统里会变得很有价值。缓存、连接、文件、任务状态、共享配置,这些东西如果归属不清楚,很容易出现生命周期问题。Rust 会要求你决定:这个值是移动进去,还是借用,还是放进共享引用计数里。每个选择都有代价,也都有语义。
建模时,我会尽量让拥有关系贴近业务关系。比如任务记录属于任务仓库,配置属于应用启动阶段,临时缓冲区属于处理函数,连接池属于服务上下文。所有权如果和业务概念对齐,代码读起来会自然很多;如果为了通过编译到处克隆或到处塞 Arc,后面反而会看不清真实关系。
struct AppState { config: AppConfig, queue: JobQueue,}
async fn handle(state: &AppState, input: JobInput) -> Result<JobId, AppError> { validate(&state.config, &input)?; state.queue.push(input).await}这里 AppState 拥有配置和队列,请求处理函数只是借用它。这个边界很普通,但它能避免每个函数都去创建自己的配置或队列。Rust 会让这些选择在签名上暴露出来,读者能直接看到谁只是借用,谁会拿走值。
借用检查不是敌人
很多 Rust 新手都会跟借用检查器较劲。明明自己知道这段代码安全,编译器却不同意。这个阶段很正常,但长期看,借用检查器不是敌人,它是在把你脑子里的假设变成代码能证明的事实。
一个常见调整是缩小可变借用范围。不要把一个可变引用拿太久,也不要在持有可变引用时再去访问原对象的其他部分。
fn normalize(values: &mut Vec<String>) { for value in values.iter_mut() { *value = value.trim().to_lowercase(); }
values.retain(|value| !value.is_empty());}这里可变操作分成两步:先逐个清理,再过滤。每一步的借用范围都很清楚。相比在一个复杂循环里又改又删,这种写法更容易通过编译,也更容易读。
遇到复杂数据结构时,有时需要调整结构,而不是和编译器硬拼。比如一个对象里既有状态又有缓存,多个方法互相借用,代码很容易打结。把状态拆小,或者把计算结果从借用关系里拿出来,通常比加一堆技巧更稳。
struct Store { items: Vec<String>,}
impl Store { fn find_index(&self, name: &str) -> Option<usize> { self.items.iter().position(|item| item == name) }
fn remove_by_name(&mut self, name: &str) -> bool { let Some(index) = self.find_index(name) else { return false; }; self.items.remove(index); true }}先找索引,再修改容器。这个写法很直接,也避免了同时持有不合适的借用。Rust 经常会让你把步骤拆清楚,拆完之后代码反而更明确。
生命周期标注也不要过早恐惧。很多业务代码不需要显式写生命周期,编译器可以推断。真正需要写时,通常说明返回值和输入引用之间有关系。这个关系如果是真实的,就写清楚;如果只是因为结构设计绕住了自己,可能应该调整数据所有权。
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str { if left.len() >= right.len() { left } else { right }}这个例子表达的是:返回的引用来自两个输入之一,所以不能比输入活得更久。生命周期不是为了让代码难懂,而是把引用关系写进类型里。工程里如果生命周期标注变得非常复杂,我会先怀疑结构是否过度借用,而不是马上继续加标注。
错误处理要认真建模
Rust 里没有把异常作为主要控制流,常见做法是用 Result 和 Option。这让错误处理很显式:函数可能失败,签名就会告诉你。你不能假装失败不存在,除非你明确选择 unwrap 或类似方式。
use std::fs;use std::path::Path;
fn read_config(path: &Path) -> Result<String, std::io::Error> { fs::read_to_string(path)}这个函数很普通,但它的失败路径是接口的一部分。调用者必须处理。对于小工具,直接把错误向上抛也可以。
fn load_names(path: &Path) -> Result<Vec<String>, std::io::Error> { let text = fs::read_to_string(path)?; let names = text .lines() .map(str::trim) .filter(|line| !line.is_empty()) .map(str::to_owned) .collect(); Ok(names)}? 让错误传播保持简洁。它不是偷懒,而是把无法处理的错误交给上层。关键在于,上层要知道如何补充上下文。
工程里通常不希望到处暴露底层错误类型。可以用一个应用级错误,把不同来源的失败整理起来。
#[derive(Debug, thiserror::Error)]enum AppError { #[error("file error: {0}")] File(#[from] std::io::Error),
#[error("config is invalid: {0}")] InvalidConfig(String),}
fn parse_workers(value: &str) -> Result<usize, AppError> { let workers = value .parse::<usize>() .map_err(|_| AppError::InvalidConfig("workers must be a number".into()))?;
if workers == 0 { return Err(AppError::InvalidConfig("workers must be positive".into())); }
Ok(workers)}错误类型不要一开始就设计得过度复杂,但要能表达关键原因。Rust 鼓励你面对失败,这对服务和工具都很有价值。很多线上问题不是因为失败发生,而是因为失败发生后没有上下文,定位不了。
错误处理还要注意边界转换。底层库的错误可以很细,但应用入口不一定要把所有细节暴露给调用方。比较好的做法是在内部保留详细上下文,对外返回稳定语义。日志和返回值不是同一件事:日志可以帮助维护者排查,返回值要帮助调用者做下一步判断。
fn load_workspace(path: &Path) -> Result<Workspace, AppError> { let config = read_config(path).map_err(|source| AppError::WorkspaceLoad { path: path.display().to_string(), source: Box::new(source), })?;
Workspace::from_config(config)}这个模式把失败发生的位置补进错误里。后面查问题时,不只是知道“读取失败”,还能知道读取的是哪个工作区。Rust 的错误传播很方便,但如果只一路 ? 到顶层,有时会丢掉上下文。该补语义的边界还是要补。
trait 边界要解决真实问题
Rust 的 trait 很强,也容易被写得过早。看到两个类型有相似方法,就想抽一个 trait;看到未来可能换实现,就想先做一层抽象。这样很容易把代码变复杂。
我更倾向于在出现真实边界时再引入 trait。比如核心逻辑需要写测试,而外部存储不适合在测试里直接调用,这时可以抽一个 trait 表达依赖。
trait TaskStore { fn save(&self, task: &Task) -> Result<(), AppError>; fn find(&self, id: &str) -> Result<Option<Task>, AppError>;}
struct TaskService<S> { store: S,}
impl<S: TaskStore> TaskService<S> { fn create(&self, title: String) -> Result<Task, AppError> { let task = Task::new(title); self.store.save(&task)?; Ok(task) }}这个 trait 的存在是为了隔离存储边界,而不是为了抽象而抽象。测试时可以放一个内存实现,生产时换成真实实现。核心服务只关心行为,不关心存储细节。
trait 设计要注意对象安全、泛型膨胀和编译时间。泛型方式性能好,编译期静态分发,但类型会传得比较多;trait object 更灵活,但有动态分发成本,也有对象安全限制。很多项目里,普通泛型已经足够,不必一开始就把所有边界做成动态对象。
fn run_with_store<S: TaskStore>(store: S) -> Result<(), AppError> { let service = TaskService { store }; service.create("build index".to_owned())?; Ok(())}这种写法适合边界不多的场景。等到需要插件式替换或运行时选择实现,再考虑 Box<dyn TaskStore>。Rust 的抽象能力很强,但工程里要让抽象为问题服务。
trait 还有一个维护上的价值:它能把测试边界固定下来。比如服务依赖时间、随机数、外部发送器、存储层时,trait 可以让核心逻辑不直接碰这些外部因素。测试时换成假的实现,生产时换成真实实现。这样做比在测试里强行操控全局状态要稳。
trait Clock { fn now(&self) -> std::time::SystemTime;}
struct TaskPolicy<C> { clock: C,}
impl<C: Clock> TaskPolicy<C> { fn is_expired(&self, deadline: std::time::SystemTime) -> bool { self.clock.now() >= deadline }}这个抽象很小,但它让时间逻辑可以测试。Rust 里很多 trait 不需要很宏大,只要能把一个不稳定外部因素隔开,就已经有价值。
Cargo workspace 让项目有层次
Rust 的工具链体验是我很喜欢的一点。cargo build、cargo test、cargo fmt、cargo clippy 都是统一入口,项目从小变大时不需要重新发明太多工具。workspace 也能帮助项目分层。
一个稍微复杂的服务,可以把核心库、命令行入口和服务入口拆成不同 crate。
[workspace]members = [ "crates/core", "crates/cli", "crates/server",]resolver = "2"核心库放业务逻辑,入口 crate 负责参数、网络或运行时。这样测试可以集中在 core,服务和命令行只是不同外壳。这个结构跟其他语言里的“核心逻辑不要锁在入口里”是同一个思路。
依赖也要控制。Rust 生态很好用,但 crate 引得太随意,编译时间和维护成本都会上来。能少引依赖就少引;引入后关注维护状态、版本兼容和功能开关。很多 crate 默认 feature 很多,不一定都需要。
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }thiserror = "1"功能开关要按实际需要开,不要看到示例怎么写就全部照搬。Rust 编译时间本来就不算轻,依赖越重越明显。长期维护时,依赖升级也应该单独处理,避免和业务改动混在一起。
workspace 里还要注意 crate 的方向。核心 crate 不应该依赖服务入口,通用库不应该反过来依赖业务应用。依赖方向一乱,编译会变慢,测试也会变重。Rust 编译器会帮你检查类型,但不会替你设计模块关系。模块关系仍然要靠工程约束。
我会让 core 尽量少依赖运行时。比如解析、校验、状态转换这些逻辑不需要 Tokio,就不要把 Tokio 带进去。server crate 可以依赖 core 和 Tokio,cli crate 可以依赖 core 和命令行解析库。这样核心逻辑既能被服务调用,也能被命令行和测试调用。
pub fn normalize_title(value: &str) -> String { value.split_whitespace().collect::<Vec<_>>().join(" ")}这个函数不关心异步、不关心 Web、不关心存储。越多核心逻辑保持这种状态,项目越容易维护。入口层可以变化,核心行为保持稳定。
并发模型要保持简单
Rust 的并发安全是它的强项。类型系统会阻止很多数据竞争,Send 和 Sync 这类 trait 会约束值能不能跨线程传递。但这不代表并发设计可以随意。没有数据竞争,不等于没有死锁、饥饿、任务泄漏和复杂状态机。
共享状态能少就少。如果需要共享,先问清楚共享的是什么,是只读配置,还是可变状态,还是任务通道。只读配置用 Arc 很自然;可变状态可能需要锁;任务流转更适合 channel。
use std::sync::Arc;use tokio::sync::Mutex;
#[derive(Default)]struct Metrics { completed: usize,}
async fn mark_completed(metrics: Arc<Mutex<Metrics>>) { let mut guard = metrics.lock().await; guard.completed += 1;}这段代码可以工作,但也要注意锁的范围。不要在持有锁时做耗时操作,更不要持有锁等待外部调用。锁保护的是状态,不应该保护整个流程。
很多时候,channel 会让并发边界更清楚。生产者发任务,worker 处理任务,状态通过消息流动,而不是大家一起改共享对象。
use tokio::sync::mpsc;
#[derive(Debug)]struct Job { id: String, payload: String,}
async fn worker(mut rx: mpsc::Receiver<Job>) { while let Some(job) = rx.recv().await { handle_job(job).await; }}这种结构把并发限制在队列和 worker 附近。其他模块只需要提交任务,不需要知道锁和线程细节。服务越大,这种边界越重要。
任务取消也要考虑。异步任务启动很容易,退出更难。服务关闭时,任务是否应该完成当前工作,是否允许中断,资源如何释放,日志如何记录,都需要设计。Rust 能保证内存安全,但不会自动替你设计运行时生命周期。
并发状态还要避免“共享一切”。有时为了方便,会把整个应用状态包进 Arc<Mutex<_>>,所有任务都进去拿。这样虽然能编译,也可能安全,但会把并发边界变得很粗。每个任务都能改所有状态,后面很难知道锁竞争和状态变化来自哪里。
更好的方式是按用途拆状态。只读配置用 Arc<AppConfig>,任务发送用 channel,统计信息用单独结构,确实需要事务性修改的部分再用锁。锁的粒度不是越细越好,但要和数据一致性关系对齐。没有一致性关系的数据,不必塞进同一把锁里。
struct RuntimeState { config: Arc<AppConfig>, jobs: mpsc::Sender<Job>, metrics: Arc<Mutex<Metrics>>,}这个结构表达了三个不同性质的状态:配置只读共享,任务通过通道发送,指标通过锁保护。它比一个巨大锁更容易理解,也更容易替换。
异步不是到处 async
Tokio 生态很成熟,但 async 不是魔法。I/O 密集服务很适合异步,比如网络请求、文件等待、定时任务、后台队列。CPU 密集任务如果直接放在 async 任务里跑,可能会阻塞运行时,让其他任务也受影响。
async fn handle_request(input: String) -> Result<String, AppError> { let parsed = parse_input(&input)?; let result = query_remote(parsed).await?; Ok(result)}这个函数表达了一个异步流程:同步解析,异步等待外部结果。边界很清楚。真正耗 CPU 的工作,可以放到专门线程池或使用运行时提供的阻塞任务接口。
async fn render_report(data: ReportData) -> Result<Vec<u8>, AppError> { let bytes = tokio::task::spawn_blocking(move || render_sync(data)) .await .map_err(|err| AppError::InvalidConfig(err.to_string()))??;
Ok(bytes)}这里不是说所有 CPU 工作都要这样处理,而是提醒异步运行时有自己的调度模型。耗时计算和 I/O 等待要区分。服务变慢时,也要看是锁竞争、外部等待、阻塞任务太多,还是运行时线程被占满。
异步代码还要注意错误传播和日志上下文。任务一旦被 spawn 出去,错误不能随便丢。后台任务失败时,至少要记录任务标识和失败原因。否则表面上服务还在跑,实际任务已经静默失败。
异步系统还要限制并发量。能够同时创建很多任务,不代表应该无限创建。外部服务、文件句柄、数据库连接、CPU 都有容量。Rust 能让任务安全地并发运行,但容量控制仍然是应用设计的一部分。
use tokio::sync::Semaphore;
async fn run_limited(jobs: Vec<Job>, limit: Arc<Semaphore>) { for job in jobs { let permit = limit.clone().acquire_owned().await.expect("semaphore closed"); tokio::spawn(async move { let _permit = permit; handle_job(job).await; }); }}这种写法让并发上限变成显式资源。实际项目里还要处理任务结果和错误,但思路很清楚:不要让任务数量完全失控。异步服务很多故障不是代码不安全,而是没有容量边界。
性能和可靠性要一起看
Rust 经常被拿来和 C++ 比性能。它确实能写出很快的程序,但我更看重的是性能和可靠性的组合。你可以有接近系统语言的控制力,同时获得更强的编译期约束。这对长期维护很有价值。
性能优化仍然要测。Rust 编译器很强,但不代表所有抽象都没有成本。克隆次数、分配次数、锁范围、异步任务数量、序列化成本,都需要通过实际数据判断。
use std::time::Instant;
fn measure<F: FnOnce()>(name: &str, f: F) { let started = Instant::now(); f(); let elapsed = started.elapsed(); println!("{name}: {} ms", elapsed.as_millis());}这种简单测量只能看大概,但能帮助定位方向。更细的分析可以用专门工具。关键是不要把“Rust 很快”当成性能结论。
克隆是 Rust 里很常见的性能点。为了通过借用检查,有时会先 clone。这并不一定错,尤其是数据很小或路径不热时,克隆能让代码简单。但热路径上大量克隆就要留意。
fn collect_names(items: &[User]) -> Vec<&str> { items.iter().map(|user| user.name.as_str()).collect()}能借用时就借用,确实需要拥有结果时再分配。Rust 会让这些选择变得显式。显式不代表永远选最省,而是知道自己在付什么成本。
性能和可靠性也会在错误处理上相遇。比如为了保留上下文,错误类型里可能带字符串、路径和来源错误;这对排查有帮助,但在极热路径上也要注意分配成本。大多数服务里,错误路径不是性能瓶颈,清楚更重要;但底层解析器或高频循环里,错误建模就需要更轻。Rust 的好处是这些代价都比较显眼,你可以根据路径做选择。
内存分配也要看场景。String、Vec、Arc 都很好用,但每次创建都有成本。处理大量小对象时,可以复用缓冲区,或者让函数接收可写目标。不要过早优化,但也不要完全忽略分配。
fn append_normalized(output: &mut String, value: &str) { output.clear(); output.push_str(value.trim()); output.make_ascii_lowercase();}这种写法不一定比返回新字符串更适合所有场景。它牺牲了一点调用便利,换来缓冲复用。工程里的性能取舍经常就是这样:热路径可以更明确地管理资源,普通路径保持简单。
测试和文档是工具链的一部分
Rust 的测试体验很直接。单元测试、集成测试、文档测试都能通过 Cargo 统一运行。这个统一体验很适合长期维护。代码改完之后,格式化、lint、测试可以形成固定流程。
pub fn normalize_name(value: &str) -> String { value.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()}
#[cfg(test)]mod tests { use super::*;
#[test] fn normalize_compacts_spaces() { assert_eq!(normalize_name(" Firefly Blog "), "firefly blog"); }}测试不要只覆盖顺利路径。错误处理是 Rust 的重点,测试也应该覆盖错误。配置不合法、输入为空、任务失败、通道关闭,这些场景都值得测。
文档测试也很有用。公共函数如果有示例,示例最好能编译运行。这样文档不会太容易过期。
/// Parses a worker count.////// ```/// let value = parse_workers("4").unwrap();/// assert_eq!(value, 4);/// ```pub fn parse_workers(value: &str) -> Result<usize, AppError> { let workers = value .parse::<usize>() .map_err(|_| AppError::InvalidConfig("workers must be a number".into()))?; Ok(workers)}文档不是为了把所有实现细节写完,而是说明边界:函数接受什么,返回什么,什么情况下失败。Rust 的类型已经表达了一部分,文档补充语义。
Clippy 和格式化也要纳入日常。cargo fmt 让风格争论少很多,cargo clippy 能指出一些常见问题。不要把 lint 当成绝对真理,但它是很好的提醒。项目可以根据实际情况允许少量例外,例外要有理由,而不是随手关掉。
持续集成里,我会固定几件事:格式检查、clippy、测试、必要时跑文档测试。Rust 编译较慢,所以流程要平衡速度和覆盖。核心库可以更频繁地测,完整服务测试可以按变更范围触发。重点是让每次改动都有基本反馈,不要等部署后才发现类型之外的行为问题。
维护成本也要算进去
Rust 的可靠性收益很明显,但维护成本也真实存在。团队是否熟悉所有权,构建时间是否可接受,依赖生态是否稳定,目标平台是否支持,调试工具是否顺手,这些都要考虑。
有些小脚本用 Python 更合适,有些性能敏感模块用 C++ 生态更成熟,有些服务用 Go 或 Java 更符合团队经验。Rust 不需要赢下所有场景。它适合那些需要长期稳定、并发安全、资源边界清晰、又希望保持较高性能的项目。
如果项目决定使用 Rust,我会尽量从几个方面维护它:保持 crate 边界清楚,控制依赖,固定格式和 lint,写好错误类型,测试覆盖关键行为,异步任务有退出策略,性能问题用数据判断。这样 Rust 的优势才能真正变成工程质量,而不是停留在语言宣传上。
发布维护也有自己的节奏。Rust 的二进制发布很方便,但构建目标、系统库依赖、交叉编译、运行参数和日志输出仍然需要整理。服务型项目还要考虑平滑停止、任务恢复和配置兼容。命令行工具则要考虑参数稳定、输出格式和退出码。语言本身解决不了这些运行期约定,它们仍然是工程的一部分。
版本升级要谨慎。Rust 生态整体活跃,依赖更新很快。升级 Tokio、serde、thiserror 或其他基础 crate 时,最好单独做,跑完测试,再观察行为。编译通过不代表行为完全没变。长期维护不是拒绝升级,而是让升级可控。
Rust 项目还需要持续关注编译时间。刚开始项目不大时,编译慢一点可以接受;依赖变多、workspace 变大之后,反馈速度会直接影响维护意愿。能拆 crate 的地方要按边界拆,不要为了拆而拆;能减少默认 feature 的依赖就减少;能把重量级依赖限制在入口层,就不要带进核心库。编译时间不是纯粹的工具问题,它会反过来影响团队愿不愿意写测试、愿不愿意频繁重构。
[dependencies]serde = { version = "1", features = ["derive"], default-features = false }这种配置不一定适合每个 crate,但它提醒我:依赖的默认能力也有成本。Rust 生态的 feature 很灵活,维护时要知道自己到底开启了什么。很多时候,少一点默认能力,换来的是更清楚的构建边界和更可控的产物。
运行期观测也不能忽略。Rust 能在编译期解决很多问题,但服务跑起来之后仍然需要日志、指标、任务状态和故障上下文。异步任务卡住、外部服务变慢、队列堆积、锁竞争变严重,这些都不是编译器能替你发现的。可靠性不是只靠类型系统,还要靠运行期反馈。写 Rust 服务时,我会把错误上下文、任务标识和关键耗时当成基础能力,而不是出问题后再补。
维护 Rust 还要接受一个事实:有些设计要经过几轮使用才知道边界是否合适。刚开始写 trait 时可能觉得很优雅,真正接入两个实现后才发现约束太窄;刚开始拆 workspace 时觉得层次清楚,后面才发现某个 crate 依赖方向不对。Rust 的编译器会帮你守很多底线,但它不会替你判断抽象是否顺手。持续重构仍然需要,只是重构时有类型系统兜着,敢动的范围会更大。
我也会给 Rust 项目保留一些“降复杂度”的机会。比如某个 trait 只有一个实现并且没有测试收益,就不急着抽;某段生命周期写得太绕,就考虑改成拥有数据;某个异步流程难以推理,就把状态机拆开。Rust 鼓励严谨,但严谨不等于把每个点都设计到极致。能让边界清楚、能让维护者读懂,往往比写出最抽象的版本更重要。
继续把边界写清楚
Rust 让我最受益的地方,是它不断提醒我把边界写清楚。值归谁,谁能改,失败怎么传,任务怎么停,状态怎么共享,抽象解决什么问题。这些问题在任何语言里都存在,只是在 Rust 里更早暴露出来。
它不是一门让代码自动变好的语言。糟糕的模块划分、过度抽象、混乱异步、随意依赖,在 Rust 里一样会变成维护负担。只是当你愿意顺着它的约束去设计时,很多长期问题会提前被看见。
这篇记录后面也会继续补。Rust 生态还在变化,异步、错误处理、构建缓存、跨平台发布、嵌入式和 WebAssembly 都有很多可以继续实践的方向。对我来说,Rust 最值得保留的不是某个语法点,而是这种工程习惯:把可靠性放到设计里,把边界写进类型里,把维护成本放在一开始就认真考虑。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
点赞和收藏保存在当前浏览器;复制链接可用于分享。