用 uuid 生成唯一标识
你会学到什么
- UUID 是什么,为什么用它给数据分配唯一 ID。
- 用
Uuid::new_v4()生成 v4(随机)UUID。 - 给业务实体(
Entity)在创建时自动分配一个新鲜 ID。 - 用
Uuid::parse_str把字符串解析回Uuid。 - 在带连字符(hyphenated)与无连字符(simple)两种文本形式之间转换。
uuid的features = ["v4"]特性开关是什么、为什么需要它。
什么是 UUID
UUID(Universally Unique Identifier)是一个 128 位的标识符,写成文本通常长这样:
550e8400-e29b-41d4-a716-446655440000
它的卖点是“几乎一定全局唯一”:不需要中心化的自增计数器,任何机器、任何 时刻独立生成的两个 UUID 撞车的概率都低到可以忽略。这让它特别适合做数据库 主键、文件名、会话 ID 等。
其中 v4 版本的绝大部分位来自随机数(所以也叫“随机 UUID”),这正是上一章
rand 那类随机数在实际库里的应用。
Cargo.toml 与特性开关
[dependencies]
uuid = { version = "1", features = ["v4"] }
uuid 把不同功能拆成了特性(features):默认只带最基础的类型,生成 v4
随机 UUID 的能力要显式打开 "v4" 特性才有(它内部依赖随机数实现)。这样
没用到的功能就不会被编进你的程序,二进制更小、编译更快。如果你还想要别的
版本(如基于时间的 v7),就再加上对应的特性。
运行代码
cd examples
cargo run -p rt_48_uuid
cargo test -p rt_48_uuid
代码组织
本章示例按职责拆成多个文件,更贴近真实项目的组织方式:
48_uuid/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs # mod 声明 + pub use 重导出 + 库级文档
├── generate.rs # 生成 UUID:new_id / new_ids / Entity
├── parsing.rs # 解析字符串与格式化:parse_id / to_simple / to_hyphenated / nil_id
└── main.rs # 瘦入口:use 库里的函数,跑一遍演示
每个模块都带有自己的单元测试(#[cfg(test)] mod tests),就近放在它所覆盖的
代码旁边。
代码讲解
生成 v4 UUID
核心就一行:
use uuid::Uuid;
pub fn new_id() -> Uuid {
Uuid::new_v4()
}
new_v4() 每次都返回一个新的随机 UUID。generate.rs 在它之上还提供了批量
生成的 new_ids(n),以及一个 Entity 实体——它的 new 会在创建时自动分配
一个 ID,调用方只管给名字:
let alice = Entity::new("alice"); // id 自动生成
解析字符串
从外部(请求参数、数据库、配置)拿到的 UUID 往往是字符串,用 parse_str
转回 Uuid,失败时返回 Result:
pub fn parse_id(s: &str) -> Result<Uuid, uuid::Error> {
Uuid::parse_str(s)
}
两种文本形式
同一个 UUID 有多种文本写法,常用的是带连字符与不带连字符两种:
id.hyphenated().to_string() // 550e8400-e29b-41d4-a716-446655440000,长度 36
id.simple().to_string() // 550e8400e29b41d4a716446655440000,长度 32
hyphenated 是默认形式(也就是 id.to_string()),可读性好;simple 更紧凑,
适合放进 URL 或文件名。需要“空 ID”占位时可以用 Uuid::nil(),它是全零值。
测试里如何应对随机性
new_v4() 是随机的,没法断言它“等于某个固定值”,所以测试要断言性质而非
具体取值:
- 版本号是 4(
id.get_version_num() == 4); - 两次生成的 ID 不相等;
- 批量生成的若干 ID 互不相同(收进
HashSet看数量)。
而解析相关的测试则用一个固定字符串,这样结果是确定的:解析后再格式化能
还原原串(round-trip)、simple 形式长度为 32 且不含 -、nil 全为零等。
常见错误
忘了开 v4 特性,编译器会报找不到 new_v4:
error[E0599]: no function or associated item named `new_v4` found for struct `Uuid`
解决办法是在 Cargo.toml 里给 uuid 加上 features = ["v4"](本章已经加好)。
另一个常见点:parse_str 返回的是 Result,别忘了处理错误分支,不要直接
unwrap 外部传入的字符串。
练习
- 给
Entity加一个from_id(id: Uuid, name)构造函数,用于从数据库读回已有 ID。 - 写一个函数,接收一批字符串,返回成功解析出的
Uuid列表(跳过非法项)。 - 试着把
simple形式再用parse_id解析回来,验证parse_str也接受无连字符形式。
小结
UUID 让你无需中心协调就能生成几乎一定唯一的标识符。Uuid::new_v4() 生成随机
ID,parse_str 解析字符串,hyphenated/simple 切换文本形式;而 features
开关决定了哪些能力被编进程序。
下一步
UUID 解决“唯一标识”,下一章我们用 sha2 计算内容的
哈希摘要,解决“内容指纹与完整性校验”的问题。
完整示例代码
下面是 examples/48_uuid/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/48_uuid/src/main.rs
//! 演示入口:调用库里拆分好的 `generate` 与 `parsing` 模块,跑一遍带标签的输出。
use rt_48_uuid::{Entity, new_id, new_ids, nil_id, parse_id, to_hyphenated, to_simple};
fn main() {
println!("== 生成 v4 随机 UUID ==");
let id = new_id();
println!("新 ID: {id}");
println!("版本号: {:?}", id.get_version_num());
println!("\n== 批量生成 ==");
for (i, id) in new_ids(3).into_iter().enumerate() {
println!(" [{i}] {id}");
}
println!("\n== 给实体分配 ID ==");
let alice = Entity::new("alice");
let bob = Entity::new("bob");
println!("{} -> {}", alice.name, alice.id);
println!("{} -> {}", bob.name, bob.id);
println!("\n== 解析与格式化 ==");
let sample = "550e8400-e29b-41d4-a716-446655440000";
match parse_id(sample) {
Ok(parsed) => {
println!("解析成功: {parsed}");
println!("带连字符: {}", to_hyphenated(&parsed));
println!("无连字符: {}", to_simple(&parsed));
}
Err(e) => println!("解析失败: {e}"),
}
println!("\n== 解析失败示例 ==");
match parse_id("not-a-uuid") {
Ok(_) => println!("不应到这里"),
Err(e) => println!("如预期解析失败: {e}"),
}
println!("\n== nil UUID ==");
println!("nil: {}", nil_id());
} examples/48_uuid/src/lib.rs
//! 用 `uuid` 这个生态库生成、解析与格式化唯一标识符(UUID)。
//!
//! UUID(Universally Unique Identifier)是一个 128 位的标识符,几乎可以保证
//! 全局唯一,常用来给数据库记录、文件、会话等分配 ID,而不必依赖中心化的
//! 自增计数器。
//!
//! 本章覆盖:
//! - 用 `Uuid::new_v4()` 生成 v4(随机)UUID;
//! - 给业务实体分配一个新鲜的 ID;
//! - 用 `Uuid::parse_str` 把字符串解析回 `Uuid`;
//! - 在带连字符(hyphenated)与无连字符(simple)两种文本形式之间转换。
//!
//! 代码按职责拆分为两个模块:
//! - [`generate`]:生成 UUID 与 `Entity` 实体;
//! - [`parsing`]:解析字符串与格式化输出。
pub mod generate;
pub mod parsing;
pub use generate::{Entity, new_id, new_ids};
pub use parsing::{nil_id, parse_id, to_hyphenated, to_simple}; examples/48_uuid/src/generate.rs
//! 生成 UUID:随机 v4 UUID,以及给业务实体分配新鲜 ID。
use uuid::Uuid;
/// 生成一个全新的 v4(随机)UUID。
///
/// v4 UUID 的绝大部分位来自随机数,因此每次调用几乎一定得到不同的值,
/// 不需要中心化的协调就能保证唯一。
///
/// ```
/// let a = rt_48_uuid::new_id();
/// let b = rt_48_uuid::new_id();
/// assert_ne!(a, b); // 两次生成几乎一定不同
/// ```
pub fn new_id() -> Uuid {
Uuid::new_v4()
}
/// 一次性生成 `n` 个互不相同的 v4 UUID。
pub fn new_ids(n: usize) -> Vec<Uuid> {
(0..n).map(|_| new_id()).collect()
}
/// 一个业务实体:拥有一个唯一 `id` 和一个 `name`。
///
/// 典型用法是“新建对象时自动分配 ID”,调用方只需提供名字。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entity {
/// 实体的唯一标识,由 [`Entity::new`] 自动生成。
pub id: Uuid,
/// 实体名字。
pub name: String,
}
impl Entity {
/// 创建一个新实体,并为它分配一个全新的 v4 UUID。
pub fn new(name: impl Into<String>) -> Self {
Self {
id: new_id(),
name: name.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Version;
#[test]
fn new_id_is_v4() {
let id = new_id();
// v4 表示“随机”版本,对应版本号 4。
assert_eq!(id.get_version(), Some(Version::Random));
assert_eq!(id.get_version_num(), 4);
}
#[test]
fn two_ids_differ() {
assert_ne!(new_id(), new_id());
}
#[test]
fn new_ids_returns_distinct_values() {
let ids = new_ids(5);
assert_eq!(ids.len(), 5);
// 收进 HashSet 后数量不变,说明没有重复。
let unique: std::collections::HashSet<_> = ids.iter().collect();
assert_eq!(unique.len(), 5);
}
#[test]
fn entity_gets_fresh_id() {
let a = Entity::new("alice");
let b = Entity::new("bob");
assert_eq!(a.name, "alice");
assert_ne!(a.id, b.id); // 每个实体的 ID 都不同
}
} examples/48_uuid/src/parsing.rs
//! 解析与格式化 UUID:字符串与 `Uuid` 之间的相互转换。
use uuid::Uuid;
/// 把字符串解析成 `Uuid`。
///
/// 接受常见的带连字符形式(如 `550e8400-e29b-41d4-a716-446655440000`),
/// 也接受无连字符的 32 位十六进制形式。解析失败返回 [`uuid::Error`]。
///
/// ```
/// let id = rt_48_uuid::parse_id("550e8400-e29b-41d4-a716-446655440000").unwrap();
/// assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
/// assert!(rt_48_uuid::parse_id("not-a-uuid").is_err());
/// ```
pub fn parse_id(s: &str) -> Result<Uuid, uuid::Error> {
Uuid::parse_str(s)
}
/// 返回带连字符(hyphenated)的标准文本形式,长度为 36(含 4 个连字符)。
///
/// 这等价于 `id.to_string()`,但写法更直白。
pub fn to_hyphenated(id: &Uuid) -> String {
id.hyphenated().to_string()
}
/// 返回无连字符(simple)的紧凑文本形式,长度为 32。
///
/// 适合放进 URL、文件名等不便出现连字符的场景。
pub fn to_simple(id: &Uuid) -> String {
id.simple().to_string()
}
/// 返回全零的 nil UUID(`00000000-0000-0000-0000-000000000000`)。
///
/// 常用作“尚未分配”或“占位”的默认值。
pub fn nil_id() -> Uuid {
Uuid::nil()
}
#[cfg(test)]
mod tests {
use super::*;
// 固定字符串让解析相关测试保持确定性。
const SAMPLE: &str = "550e8400-e29b-41d4-a716-446655440000";
#[test]
fn parse_round_trip() {
let id = parse_id(SAMPLE).unwrap();
// 解析后再格式化,应当还原成原字符串。
assert_eq!(id.to_string(), SAMPLE);
// 再解析一次也应得到同一个值。
assert_eq!(parse_id(&id.to_string()), Ok(id));
}
#[test]
fn parse_rejects_garbage() {
assert!(parse_id("not-a-uuid").is_err());
}
#[test]
fn simple_form_has_no_hyphen() {
let id = parse_id(SAMPLE).unwrap();
let simple = to_simple(&id);
assert_eq!(simple.len(), 32);
assert!(!simple.contains('-'));
}
#[test]
fn hyphenated_form_matches_to_string() {
let id = parse_id(SAMPLE).unwrap();
assert_eq!(to_hyphenated(&id), id.to_string());
assert_eq!(to_hyphenated(&id).len(), 36);
}
#[test]
fn nil_is_all_zeros() {
let nil = nil_id();
assert!(nil.is_nil());
assert_eq!(nil.as_bytes(), &[0u8; 16]);
}
} examples/48_uuid/Cargo.toml
[package]
name = "rt_48_uuid"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
uuid = { version = "1", features = ["v4"] }