常用生态库 intermediate 15 分钟 更新 2026-06-15

用 uuid 生成唯一标识

用 uuid 生成 v4 随机 UUID、解析与格式化,给实体分配唯一 ID。

用 uuid 生成唯一标识

你会学到什么

  • UUID 是什么,为什么用它给数据分配唯一 ID。
  • Uuid::new_v4() 生成 v4(随机)UUID。
  • 给业务实体(Entity)在创建时自动分配一个新鲜 ID。
  • Uuid::parse_str 把字符串解析回 Uuid
  • 在带连字符(hyphenated)与无连字符(simple)两种文本形式之间转换。
  • uuidfeatures = ["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"] }