从 Graydon Hoare 2006 年的实验到今天的 Rust 生态,了解无需垃圾回收的内存安全如何重塑系统编程。

本文讲述一个聚焦的起源故事:Graydon Hoare 的个人实验如何成长为 Rust,以及为什么 Rust 的设计选择足以重塑系统编程的预期。
“系统编程”贴近机器层面,也贴近产品的风险面。它出现在浏览器、游戏引擎、操作系统组件、数据库、网络和嵌入式软件中——这些地方通常需要:
历史上,这种组合促使团队选择 C 和 C++,并辅以大量的规则、审查和工具来减少与内存相关的错误。
Rust 的标语说起来简单但实现难度大:
无需垃圾回收的内存安全。
Rust 旨在防止常见失败,如 use-after-free、double-free 以及许多类型的数据竞争——而不依赖一个周期性暂停程序以回收内存的运行时。相反,Rust 把大量工作移到编译期,通过所有权和借用来完成。
你会看到历史(从早期想法到 Mozilla 的参与)和关键概念(所有权、借用、生命周期、安全与不安全)用通俗语言解释。
你不会得到完整的 Rust 教程、详尽的语法导览或逐步项目设置。把这篇文章当作解释 Rust 设计背后“为什么”的文章,包含足够的示例让这些想法具体化。
作者备注: 完整文章目标约 3,000 字,留出空间给简短示例而不变成参考手册。
Rust 并不是从委员会设计出来的“下一个 C++”。它始于 Graydon Hoare 在 2006 年的一次个人实验——当时他独立开展这项工作,直到逐渐引起更广泛的关注。这个起源很重要:许多早期设计决策更像是在解决日常痛点,而不是去“赢得”语言理论比赛。
Hoare 在探索如何在不依赖垃圾回收的前提下编写低层、高性能的软件,同时也避免 C/C++ 中最常见的崩溃和安全漏洞。对系统程序员来说,这种张力很熟悉:
Rust 的“无 GC 的内存安全”方向一开始并不是营销口号,而是一个设计目标:保持适用于系统工作的性能特性,同时让许多类别的内存错误难以表达。
有人会问,这为什么不能只是“更好的 C/C++ 编译器”。静态分析、sanitizer 和更安全的库可以防止很多问题,但它们通常不能保证内存安全。底层语言允许的某些模式很难或不可能从外部完全管控。
Rust 的押注是把关键规则移到语言和类型系统中,让安全成为默认结果,同时在清晰标注的逃生口允许手工控制。
关于 Rust 最早期的一些细节常以轶事形式流传(常在演讲和采访中被反复引用)。在讲述这个起源故事时,把广泛记录的里程碑——比如 2006 年的启动日期和后来 Mozilla Research 的采用——与个人回忆和二次叙述区分开会更有帮助。
查阅早期 Rust 文档和设计笔记、Graydon Hoare 的演讲/采访,以及描述项目被采纳和目标如何被框定的 Mozilla/Servo 时代帖子,会是很好的第一手资料来源。可参考的进一步阅读在 /blog 中的相关链接。
系统编程常常意味着贴近硬件。这种贴近性让代码快速且资源高效,但也使得内存错误代价惨重。
几个经典错误反复出现:
这些错误并不总是显而易见。程序可能“正常运行”数周,然后只有在罕见的时序或输入模式下才崩溃。
测试只能证明你测试过的用例是正确的。内存错误常常藏在你没有测试到的用例中:异常输入、不同硬件、微小的时序变化或新编译器版本。它们在多线程程序中尤其可能是非确定性的——尤其是当你加上日志或用调试器运行时,这些错误有时会消失。
当内存出现问题,你得到的不是简单的错误,而是被破坏的状态、不可预测的崩溃和攻击者积极寻找的安全漏洞。团队为追踪这些难以重现并更难诊断的故障投入大量精力。
低层软件并不总是能“为安全支付”沉重的运行时检查或不断的内存扫描的代价。目标更像是从公共工具间借一个工具:你可以自由使用,但规则必须清楚——谁持有、谁能共享、何时归还。传统系统语言把这些规则留给人的纪律来维护。Rust 的起源史就是从质疑这种权衡开始的。
垃圾回收(GC)是常见的防止内存错误的方式。运行时跟踪对象是否可达,自动回收不可达对象,这可以消除整类问题——use-after-free、double-free,很多内存泄漏也能被避免,因为程序不会以同样的方式“忘记”清理。
GC 并非“坏”,但它改变了程序的性能曲线。大多数收集器会引入:
对于很多应用(网络后端、业务软件、工具链),这些成本是可接受甚至不可见的。现代 GC 很出色,而且能显著提高开发效率。
在系统编程中,最坏情况常常是关键。浏览器引擎需要平滑的渲染;嵌入式控制器可能有严格的时序约束;低延迟服务器可能要在负载下保持尾延迟。对这些环境而言,“通常快”不如“始终可预测”更有价值。
Rust 的大承诺是:保持 类似 C/C++ 的内存与数据布局控制,同时在 不依赖垃圾回收器 的前提下提供 内存安全。目标是让性能可预测——同时把安全代码作为默认情况。
这并不是说 GC 低人一等,而是押注存在一个重要的中间地带:既需要底层控制又需要现代安全保证的软件。
所有权是 Rust 最简单但最重要的想法:每个值都有一个唯一的所有者,负责在不再需要时清理它。
这条规则取代了 C/C++ 程序员常在脑中跟踪的“谁来释放这块内存”的大量手工记账。Rust 让清理变得可预测。
当你复制某样东西时,你得到两个独立的版本。当你移动某样东西时,你把原件交给了别人——移动之后,旧变量不再被允许使用它。
Rust 默认把许多堆分配的值(比如字符串、缓冲区或向量)视为可移动的。盲目复制它们不仅可能昂贵,更重要的是容易混淆:如果两个变量都认为自己“拥有”同一个分配,你就为内存错误埋下隐患。
下面是一个小伪代码示例(保留原始代码块,不翻译代码内容):
buffer = make_buffer()
ownerA = buffer // ownerA owns it
ownerB = ownerA // move ownership to ownerB
use(ownerA) // not allowed: ownerA no longer owns anything
use(ownerB) // ok
// when ownerB ends, buffer is cleaned up automatically
因为总是只有一个所有者,Rust 知道值什么时候应该被清理:当它的所有者超出作用域时。也就是说 自动内存管理(你不必处处调用 free()),却不需要垃圾回收器周期性地扫描程序并回收未使用的内存。
这个所有权规则阻止了大量经典问题:
Rust 的所有权模型不仅鼓励更安全的习惯——它使许多不安全的状态变得不可表示,这是 Rust 其余安全特性的基础。
所有权解释了谁“拥有”一个值。借用解释了程序的其他部分如何在不取得所有权的情况下暂时使用该值。
当你借用某样东西时,你得到它的一个引用。原所有者仍然负责释放内存;借用者只是被允许在一段时间内使用它。
Rust 有两种借用:
&T):只读访问。&mut T):读写访问。Rust 的核心借用规则简单却强大:
这条规则防止了常见的错误:程序的一部分在读取数据时另一部分在对其进行修改。
引用只有在其指向的对象仍然存在时才是安全的。Rust 把这种持续时间称为生命周期 —— 引用被保证有效的时间段。
使用这个概念不需要太多形式化:引用不能在所有者消失之后仍然存在。
Rust 通过借用检查器在编译期强制这些规则。与其指望测试发现坏引用或危险的变更,Rust 会拒绝构建可能错误使用内存的代码。
把它想象为一个共享文档:
并发是“在我机器上能运行”的 bug 的温床。当两个线程同时运行时,尤其是共享数据时,它们可能以出乎意料的方式互相影响。
数据竞争发生在:
结果不仅仅是“输出错误”。数据竞争会破坏状态、导致崩溃或产生安全漏洞。更糟的是,它们可能是间歇性的:在你加入日志或在调试器下运行时,问题可能消失。
Rust 采取了不寻常的立场:与其信任每个程序员在任何时候都能记住规则,它更倾向于让许多不安全的并发模式在安全代码中无法表示。
从高层看,Rust 的所有权与借用规则不仅限于单线程代码。它们也影响你允许跨线程共享什么。如果编译器无法证明共享访问是受协调的,它就不会让代码通过编译。
这就是人们所说的 Rust 中的 “安全并发”:你仍然可以编写并发程序,但大量“糟糕,两线程同时写了同一块东西”的错误会在程序运行前被捕获。
想象两个线程对同一个计数器做递增:
Rust 并不禁止底层并发技巧。它把它们隔离出来。如果你确实需要做编译器无法验证的操作,可以使用 unsafe 块,它像一个警告标签:“这里需要人工负责”。这种区分让代码库的大部分保持在更安全的子集内,同时在必要时仍允许运行级别的能力。
Rust 的安全声誉并非绝对,而更准确的说法是 Rust 把安全与不安全之间的边界明确化并便于审计。
大多数 Rust 代码属于“安全 Rust”。在这里,编译器强制规则,防止常见的内存错误:use-after-free、double-free、悬指针和数据竞争。你仍可能写出逻辑错误,但不能通过常规语言特性意外破坏内存安全。
要点是:安全 Rust 并不等于“慢”。许多高性能程序完全用安全 Rust 编写,因为一旦编译器能信任规则被遵守,就可以做出激进的优化。
unsafe 存在是因为系统编程有时需要编译器无法在一般情况下证明安全的能力。典型原因包括:
使用 unsafe 并不会关闭所有检查。它只允许一小组通常被禁止的操作(比如解引用原始指针)。
Rust 要求你标注不安全的函数和不安全的代码块,使风险在代码审查中一目了然。一种常见模式是把微小的“unsafe 核心”包装成安全 API,这样程序的大部分保留在安全 Rust 中,而一小段经过良好定义的代码负责维护必要的不变式。
把 unsafe 当作电动工具:
unsafe 块 保持小且局部化。unsafe 更改要求 额外审查。做得好时,unsafe Rust 成为访问仍需人工精确控制的系统编程部分的可控接口——而不会剥夺 Rust 在其它地方带来的安全收益。
Rust 之所以变得“真实”,不是因为纸面上有巧妙想法,而是因为 Mozilla 把这些想法投入到了实际考验中。
Mozilla Research 寻找的方法是用更少的安全漏洞来构建性能关键的浏览器组件。浏览器引擎异常复杂:它们解析不受信任的输入、管理大量内存并运行高度并行的工作负载。这些组合使内存安全缺陷和竞态条件既常见又代价高昂。
支持 Rust 与这一目标一致:保持系统编程的速度,同时减少整类漏洞。Mozilla 的参与也向更广泛的社区传递出信号:Rust 不再只是 Graydon Hoare 的个人实验,而是一门能在地球上最苛刻的代码库之一上接受考验的语言。
Servo(实验性浏览器引擎项目)成为了在大规模上尝试 Rust 的高调场所。重点并非“赢得浏览器市场”,而是作为一个实验室来评估语言特性、编译器诊断和工具链在真实约束下的表现:构建时间、跨平台支持、开发者体验、性能调优以及并行性下的正确性。
更重要的是,Servo 帮助塑造了语言周边的生态:库、构建工具、约定和调试实践,这些在超越玩具程序时非常重要。
真实项目会形成反馈闭环,这是语言设计无法伪造的。当工程师遇到摩擦——不清楚的错误信息、缺失的库、别扭的模式——这些痛点会迅速暴露。随着时间推移,这种持续的压力帮助 Rust 从有前景的概念成长为团队可以在性能关键软件上信任的工具。
若想在这个阶段之后进一步探索 Rust 的演进,请参见 /blog/rust-memory-safety-without-gc。
Rust 位于中间地带:它追求与 C/C++ 相当的性能和控制,同时努力消除那些语言经常把责任留给纪律、测试和运气的错误类别。
在 C 和 C++ 中,开发者直接管理内存——分配、释放并确保指针保持有效。这种自由很强大,但也容易导致 use-after-free、double-free、缓冲区溢出和微妙的生命周期错误。编译器通常会信任程序员。
Rust 翻转了这种关系。你仍然可以做底层控制(栈 vs 堆的选择、可预测的数据布局、显式的所有权传递),但编译器会强制关于谁拥有一个值以及引用能活多久的规则。与其说“要小心指针”,Rust 更像是要你“向编译器证明安全”,并且在安全 Rust 中不会编译出可能破坏这些保证的代码。
垃圾回收语言(如 Java、Go、C# 或许多脚本语言)用自动回收替代了手动内存管理:当对象不再可达时就会被回收,这对开发效率是巨大的提升。
Rust 的承诺“无 GC 的内存安全”意味着你不需要为运行时垃圾回收器付出代价,这在需要严格控制延迟、内存占用、启动时间或在受限环境运行时尤其重要。权衡在于你要明确地建模所有权,并让编译器来强制执行它。
Rust 起初会感觉更难,因为它要求一种新的思维模型:你需要以所有权、借用和生命周期来思考,而不是仅仅“传个指针希望没事”。早期摩擦通常出现在建模共享状态或复杂对象图时。
Rust 对于构建安全敏感且性能关键的软件——浏览器、网络、密码学、嵌入式、对可靠性有严格要求的后端服务——常常表现尤为出色。如果你的团队更看重最快的迭代速度而不是底层控制,GC 语言可能仍然更合适。
Rust 并非万能替代品;当你想要 C/C++ 级性能并希望依赖可依赖的安全保证时,它是一个很强的选择。
Rust 并不是靠“比 C++ 更友好”博取注意力,而是通过坚称低层代码可以同时快速、内存安全并且明确成本来改变了对话。
在 Rust 出现之前,团队通常把内存错误当作性能的代价,然后通过测试、代码审查和事后修复来管理风险。Rust 做出了不同的选择:把常见规则(谁拥有数据、谁能修改它、何时必须保持有效)编码到语言中,这样整类错误会在编译期被拒绝。
这种转变重要的地方在于它并不要求开发者“完美”,而是要求他们清晰——然后让编译器来强制这种清晰。
Rust 的影响体现在一系列信号上,而不是单一头条:对发布性能敏感软件的公司的兴趣增长、在大学课程中的增加、以及感觉不再像“研究项目”的工具链(包管理、格式化、lint、文档工作流开箱即用)。
这并不意味 Rust 永远是最佳选择——但它确实使得“默认启用安全”成为现实可期,而不是一种奢侈。
Rust 常被评估用于:
“新标准”并不意味着每个系统都会被重写为 Rust,而是门槛提升了:团队越来越多地会问,为什么要接受默认不安全的内存模型? 即便不采用 Rust,它的模型也推动生态系统重视更安全的 API、更清晰的不变式以及更好的正确性工具。
如果你想读更多类似的工程故事,请浏览 /blog 中的相关文章。
Rust 的起源故事有一条简单的主线:一个人的副项目(Graydon Hoare 对新语言的实验)直面了一个顽固的系统编程问题,解决方案既严格又实用。
Rust 重构了许多开发者认为不可避免的权衡:
实际的改变不仅仅是“Rust 更安全”。更重要的是,安全可以成为语言的默认属性,而不是通过代码审查和测试的尽力而为。
如果你感兴趣,不需要大规模重写就能体验 Rust 的感觉。
从小做起:
如果想温和入门,选择一个“薄片”目标——例如“读取文件、转换内容、写回输出”——并专注写清晰的代码而不是耍聪明。
在更大的产品中把 Rust 组件作为原型也很常见:把周边部分(管理界面、仪表盘、控制平面、简单 API)快速迭代,同时把核心系统逻辑做得谨慎严谨。像 Koder.ai 这样的平台可以通过聊天驱动的工作流加速这类“胶水”开发 —— 让你快速生成 React 前端、Go 后端和 PostgreSQL 模式,然后导出源码并用清晰的边界将其与 Rust 服务集成。
如果你想要第二篇文章,哪种内容最有用?
unsafe回复并说明你的背景(你构建什么、目前用什么语言、优化的目标是什么),我会据此把下一部分量身定制。
系统编程是贴近硬件和产品高风险面的工作 —— 比如浏览器引擎、数据库、操作系统组件、网络以及嵌入式软件。
它通常要求 可预测的性能、底层内存/控制能力 和 高可靠性,在这些场景下崩溃和安全漏洞代价特别高。
这意味着 Rust 旨在在 不依赖运行时垃圾回收器 的前提下,防止常见的内存错误(例如 use-after-free 和 double-free)。
Rust 把大量安全检查移到编译时,通过所有权和借用规则来实现,而不是在运行时由收集器扫描并回收内存。
像 sanitizer 和静态分析这样的工具可以发现许多问题,但当语言本身允许自由地操纵指针和生命周期时,工具通常无法保证内存安全。
Rust 把关键规则内置到语言和类型系统里,让编译器能够默认拒绝整类错误,同时在必要时保留明确标注的逃生口(escape hatch)。
GC 会带来运行时开销,更重要的是在某些系统工作负载下会导致延迟不可预测(例如出现暂停或在不合适的时间做回收工作)。
在浏览器、实时控制器或低延迟服务等领域,最坏情况行为很重要,因此 Rust 的目标是既保证安全又保持更可预测的性能特性。
所有权的意思是每个值都有一个唯一的“负责方”(所有者)。当所有者超出作用域时,值会被自动清理。
这使得清理变得可预测,并避免了两个地方都认为自己应该释放同一块内存的情况。
“移动”会把所有权从一个变量转移到另一个变量;原来的变量之后不能再使用该值。
这样可以避免出现“两个所有者指向同一分配”的意外情况,而这是手动内存语言中导致 double-free 和 use-after-free 的常见根源。
借用允许代码通过引用临时使用某个值而不取得所有权。
核心规则是:多读或单写——你可以同时拥有多个共享引用(&T),或者一个可变引用(&mut T),但不能同时两者兼得。这可以防止大量在读取时被修改或别名访问导致的错误。
生命周期是“这个引用有效多久”。Rust 要求引用不能比它指向的数据活得更久。
借用检查器在编译时强制这些规则,因此有可能产生悬 dangling 引用的代码会在编译期被拒绝。
数据竞争发生在多个线程同时访问同一内存、至少一个是写操作且没有协调(如锁)时。
Rust 的所有权/借用规则同样影响并发场景,使得不安全的共享模式在安全代码中难以(或不可能)表达,从而推动你使用显式同步或消息传递等方式来协调访问。
大部分代码写在安全 Rust(safe Rust)中,编译器在此模式下会强制内存安全规则。
unsafe 是明确标注的逃生通道,用于编译器无法在一般情况下证明安全的操作(比如某些 FFI 调用或底层原语)。一种常见做法是把 unsafe 保持得很小并用安全 API 包裹,这样在代码审查时更容易审核。