一句话总结
Rust 的全局常量用 const, 全局静态值用 static.
但可变的全局状态在 Rust 里是"危险品"--因为它天然违背借用规则(谁都能改, 无法保证独占).
现代 Rust 用 OnceLock / LazyLock 安全地实现"全局单例 + 延迟初始化", 避开了裸 static mut 的陷阱.
const 与 static: 两种全局值
先分清两个最基础的全局声明:
|
const |
static |
|
|---|---|---|
| 本质 | 编译期常量, 使用处内联(像每处都复制一份) | 程序中唯一的内存位置, 有固定地址 |
| 生命周期 | 无固定地址, 概念上"无处不在" | 'static, 活整个程序 |
| 可变性 | 永远不可变 | 默认不可变; static mut 可变但 unsafe |
| 类比 Go | Go 的 const |
Go 的包级 var(但默认不可变) |
绝大多数"全局只读配置"用 const 即可. static 用在需要"唯一固定地址"的场景(比如要取它的引用).
为什么"全局可变状态"在 Rust 里很难
这是本篇的思想核心. 在 Go 里, 一个包级 var counter int 谁都能读写, 稀松平常(并发下自己加锁). 但在 Rust 里, 这种"全局可变变量"直接撞上借用规则的铁律:
全局可变 = 天然的'共享 + 可变'
一个可变全局变量, 意味着程序里任何地方都能拿到它的可变引用--这正是借用规则要禁止的"既共享又可变".
更要命的是在多线程下: 多个线程同时改一个全局变量就是赤裸裸的数据竞争. 所以裸的 static mut 在 Rust 里访问它必须用 unsafe, 编译器等于在说: "我没法保证它安全, 自己担责."
|
static mut 像一栋楼里没有任何门锁的公共储物间: 谁都能进, 谁都能改, 还可能两个人同时伸手.
Rust 看到它就亮红灯(unsafe), 逼着换一种"带门禁"的方案.
下面这些工具, 就是给全局状态装上门禁.
OnceLock: 安全的全局单例
真实需求往往不是"频繁修改的全局变量", 而是"全局只初始化一次, 之后只读"的单例--比如全局配置, 连接池, 正则表达式. std::sync::OnceLock 正为此设计: 它是一个"只能被写入一次"的容器, 且线程安全.
|
对应 Go 的 sync.Once
OnceLock + get_or_init 的语义, 正是 Go 里 sync.Once + 包级 var 的组合: 保证某段初始化逻辑在并发下恰好执行一次.
区别是 Rust 把"只写一次"做进了类型--拿不到它的可变引用, 只能 init 一次, 之后只读, 从类型上杜绝了"初始化后被意外篡改".
LazyLock: 声明即带初始化逻辑
如果初始化逻辑固定, LazyLock(Rust 1.80+ 稳定)更简洁--它把初始化闭包直接写在声明处, 首次访问时自动执行:
|
选型小结
- 编译期常量 →
const - 全局只读, 初始化逻辑固定 →
LazyLock(最省心) - 全局只读, 初始化需运行期参数/时机 →
OnceLock+get_or_init - 全局可变 → 用
static配Mutex/RwLock/原子类型(见下), 不要用static mut
历史上社区曾长期依赖第三方库 lazy_static / once_cell 来做这些, 如今标准库的 OnceLock/LazyLock 已能覆盖绝大多数场景, 优先用标准库.
确实需要全局可变: 加锁或用原子
如果真的需要可变的全局状态, 正确做法是把它包进线程安全的内部可变容器--这正好复用了第 10 篇的工具:
|
闭环: 全局状态也逃不开 Sync
注意: 能放进 static 的类型必须是 Sync(第 09 篇)--因为全局变量天然可被多线程访问.
Mutex, 原子类型都是 Sync 的, 所以能用; 而 RefCell 不是 Sync, 不能放进 static. 这再次印证: Send/Sync 是贯穿 Rust 并发的那条主线, 连全局状态都由它把关.
快速回顾
- const vs static: const 是内联的编译期常量; static 是有唯一固定地址,
'static的静态变量. - 全局可变很难: 它天然是"共享 + 可变", 违背借用规则; 裸
static mut需unsafe, 应避免. - OnceLock: 只写一次的线程安全容器,
get_or_init实现单例, 对应 Go 的sync.Once. - LazyLock: 声明处绑定初始化逻辑, 首次访问触发, 最省心; 优先用标准库而非旧的 lazy_static.
- 真要全局可变: 用
static+Mutex/RwLock/原子类型, 绝不用static mut. - Sync 把关: 放进 static 的类型必须是 Sync, 全局状态也被 Send/Sync 这条主线约束.
动手练习
- 说出
const MAX: u32 = 100和static MAX: u32 = 100的两点区别.
参考答案
- 内存:
const没有固定地址, 使用处被内联(概念上每处复制一份);static在程序里有唯一固定的内存地址, 可以取它的引用&MAX. - 生命周期:
static的生命周期是'static(活整个程序),const不是"一个有地址的值", 不谈生命周期.
日常全局只读配置多数用 const 就够, 需要稳定地址/引用时才用 static.
- 为什么
static mut COUNTER: u32 = 0;的访问必须放在unsafe块里?
参考答案
因为可变全局变量天然是"共享 + 可变"--程序任何地方都能拿到它的可变引用, 这违背借用规则; 在多线程下, 多个线程同时读写它就是数据竞争. 编译器无法静态保证对它的访问安全, 所以要求用 unsafe 把责任显式交给程序员.
现代 Rust 几乎从不用 static mut, 而用 static + Mutex/原子类型替代.
- 用
LazyLock定义一个全局的, 延迟初始化的Vec<i32>(内容为 1..=5), 并在 main 中打印它的和.
参考答案
|
初始化闭包只在首次访问 NUMBERS 时执行一次, 之后复用. 线程安全由 LazyLock 保证.
- 为什么可以把
Mutex<u32>放进static, 却不能放RefCell<u32>?
参考答案
因为 static 变量天然可被多个线程访问, 要求其类型是 Sync(第 09 篇). Mutex 是 Sync(它用锁保证多线程安全访问), 所以能放; RefCell 不是 Sync(它的借用记账非线程安全), 编译器会拒绝把它放进 static.
这说明全局状态同样被 Send/Sync 这条主线把关--需要全局可变就用 Mutex 或原子类型这类 Sync 的内部可变容器.
- 需求: 实现一个全局的, 首次使用时才初始化的配置对象, 且初始化需要读取一个运行期才知道的值. 该选
LazyLock还是OnceLock? 为什么?
参考答案
选 OnceLock + get_or_init. 因为初始化依赖"运行期才知道的值"--LazyLock 的初始化闭包在声明处就写死了, 拿不到运行期参数; 而 OnceLock 把初始化推迟到调用 get_or_init(|| ...) 时, 闭包里可以捕获运行期得到的数据.
规律: 初始化逻辑/数据在编译期就固定 → LazyLock(更简洁); 初始化需要运行期的参数或时机 → OnceLock(更灵活).
进阶系列完成
本系列走完了 Rust 最硬核的一段: 生命周期, 智能指针, 并发三大支柱, 以及它们背后"所有权规则贯穿始终"的设计思想.
现在能读懂复杂的 borrow checker 报错, 能为共享数据选对工具, 能从设计层面理解 Send/Sync 为何让并发"无畏". 接下来可进入**「Rust 知识点突破」查漏补缺, 或挑战「异步 Rust 与 Tokio」**--它正建立在本系列的 Send/Sync 与生命周期之上.