从零到一构建系统级工具的完整过程:我的第一个Rust项目复盘

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

从零到一构建系统级工具的完整过程:我的第一个Rust项目复盘

cover

一、为什么选择从零构建:教程项目的局限

跟着教程写Rust项目,编译通过了就觉得"学会了"。但真正从零开始——没有教程的步骤指引,没有现成的项目结构,只有自己定义的需求——才发现差距有多大:不知道怎么组织代码、不知道错误处理该用什么模式、不知道测试该怎么写、不知道CI怎么配。

我决定从零构建一个系统级工具:dust——一个磁盘使用分析工具,类似du但带可视化。这个选择的原因:功能明确、涉及文件系统操作、需要递归遍历、有格式化输出——刚好覆盖Rust系统编程的核心场景。

本文复盘整个构建过程,重点记录踩过的坑和学到的经验。

二、项目规划与设计

2.1 开发阶段规划

graph LR
    A[需求定义] --> B[MVP实现]
    B --> C[功能完善]
    C --> D[错误处理]
    D --> E[性能优化]
    E --> F[测试与CI]
    F --> G[发布]

2.2 需求定义

MVP(最小可行产品)只需要三个功能:

  1. 递归扫描目录,计算每个子目录的大小
  2. 按大小排序输出
  3. 支持深度限制
// 最初的需求定义,直接写在main.rs里
fn main() {
    let args: Vec<String> = std::env::args().collect();
    let path = args.get(1).unwrap_or(&".".to_string()).clone();
    let max_depth: usize = args.get(2)
        .and_then(|s| s.parse().ok())
        .unwrap_or(5);

    let result = scan_directory(&path, max_depth, 0);
    match result {
        Ok(entries) => {
            for entry in entries {
                println!("{} {}", entry.size, entry.path);
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

三、MVP实现:先跑起来

3.1 核心数据结构

use std::path::PathBuf;

#[derive(Debug)]
struct DirEntry {
    path: PathBuf,
    size: u64,
    is_dir: bool,
}

fn scan_directory(
    path: &str,
    max_depth: usize,
    current_depth: usize,
) -> Result<Vec<DirEntry>, std::io::Error> {
    let mut entries = Vec::new();
    let root = std::path::Path::new(path);

    if !root.is_dir() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::NotADirectory,
            format!("{} is not a directory", path),
        ));
    }

    scan_recursive(root, max_depth, current_depth, &mut entries)?;
    Ok(entries)
}

fn scan_recursive(
    dir: &std::path::Path,
    max_depth: usize,
    depth: usize,
    results: &mut Vec<DirEntry>,
) -> Result<(), std::io::Error> {
    if depth > max_depth {
        return Ok(());
    }

    let mut dir_size: u64 = 0;

    for entry in std::fs::read_dir(dir)? {
        let entry = entry?;
        let metadata = entry.metadata()?;

        if metadata.is_dir() {
            let sub_path = entry.path();
            scan_recursive(&sub_path, max_depth, depth + 1, results)?;
            // 子目录大小在递归后累加
            if let Some(sub_entry) = results.iter()
                .find(|e| e.path == sub_path)
            {
                dir_size += sub_entry.size;
            }
        } else {
            dir_size += metadata.len();
        }
    }

    results.push(DirEntry {
        path: dir.to_path_buf(),
        size: dir_size,
        is_dir: true,
    });

    Ok(())
}

3.2 MVP的问题

MVP能跑,但问题很多:

  • 错误处理太粗糙,unwrap到处都是
  • 递归中查找子目录大小效率很低(O(n)查找)
  • 没有权限错误处理(Permission denied直接panic)
  • 输出格式不好看

四、重构:从"能跑"到"好用"

4.1 错误处理重构

use anyhow::{Context, Result};

fn scan_directory(path: &str, max_depth: usize) -> Result<Vec<DirEntry>> {
    let root = std::path::Path::new(path);
    anyhow::ensure!(root.is_dir(), "{} is not a directory", path);

    let mut entries = Vec::new();
    scan_recursive(root, max_depth, 0, &mut entries)?;
    Ok(entries)
}

fn scan_recursive(
    dir: &std::path::Path,
    max_depth: usize,
    depth: usize,
    results: &mut Vec<DirEntry>,
) -> Result<()> {
    if depth > max_depth {
        return Ok(());
    }

    let mut dir_size: u64 = 0;

    for entry in std::fs::read_dir(dir)
        .with_context(|| format!("Cannot read dir: {}", dir.display()))?
    {
        let entry = match entry {
            Ok(e) => e,
            Err(e) => {
                // 权限错误不中断,跳过并记录
                eprintln!("Warning: {}", e);
                continue;
            }
        };

        let metadata = match entry.metadata() {
            Ok(m) => m,
            Err(e) => {
                eprintln!("Warning: {} - {}", entry.path().display(), e);
                continue;
            }
        };

        if metadata.is_dir() {
            let sub_path = entry.path();
            scan_recursive(&sub_path, max_depth, depth + 1, results)?;
        } else {
            dir_size += metadata.len();
        }
    }

    results.push(DirEntry {
        path: dir.to_path_buf(),
        size: dir_size,
        is_dir: true,
    });

    Ok(())
}

4.2 用HashMap替代线性查找

use std::collections::HashMap;

fn scan_with_sizes(
    dir: &std::path::Path,
    max_depth: usize,
) -> Result<HashMap<PathBuf, u64>> {
    let mut sizes = HashMap::new();
    scan_recursive_v2(dir, max_depth, 0, &mut sizes)?;
    Ok(sizes)
}

fn scan_recursive_v2(
    dir: &std::path::Path,
    max_depth: usize,
    depth: usize,
    sizes: &mut HashMap<PathBuf, u64>,
) -> Result<u64> {
    if depth > max_depth {
        return Ok(0);
    }

    let mut dir_size: u64 = 0;

    for entry in std::fs::read_dir(dir)
        .with_context(|| format!("Cannot read: {}", dir.display()))?
    {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        let metadata = match entry.metadata() {
            Ok(m) => m,
            Err(_) => continue,
        };

        if metadata.is_dir() {
            let sub_size = scan_recursive_v2(
                &entry.path(), max_depth, depth + 1, sizes
            )?;
            dir_size += sub_size;
        } else {
            dir_size += metadata.len();
        }
    }

    sizes.insert(dir.to_path_buf(), dir_size);
    Ok(dir_size)
}

4.3 可视化输出

fn display_tree(
    sizes: &HashMap<PathBuf, u64>,
    root: &std::path::Path,
    max_depth: usize,
) {
    let root_size = sizes.get(root).copied().unwrap_or(0);
    let bar_width = 40;

    // 按大小排序
    let mut entries: Vec<_> = sizes.iter().collect();
    entries.sort_by(|a, b| b.1.cmp(a.1));

    for (path, &size) in &entries {
        if !path.starts_with(root) {
            continue;
        }
        let relative = path.strip_prefix(root).unwrap_or(path);
        let ratio = if root_size > 0 { size as f64 / root_size as f64 } else { 0.0 };
        let filled = (ratio * bar_width as f64) as usize;

        let bar: String = "█".repeat(filled)
            + &"░".repeat(bar_width - filled);

        println!("{:>10} │{}│ {}",
            format_size(size), bar, relative.display()
        );
    }
}

五、架构权衡与边界分析

5.1 同步 vs 异步

文件系统遍历用同步API更简单,异步的收益不大(磁盘IO不是网络IO那种高并发场景)。如果后续需要并发扫描多个目录,可以用rayon而非tokio

5.2 递归 vs 迭代

递归实现简洁,但深度目录可能导致栈溢出。实际使用中,max_depth限制在20以内是安全的。如果需要处理无限深度,应改为迭代实现(用显式栈)。

5.3 精度 vs 性能

metadata()获取的文件大小不是精确的磁盘占用(未考虑块对齐和稀疏文件)。精确计算需要statvfs等系统调用,但MVP阶段用metadata()足够。

六、总结

从零构建系统级工具的关键经验:先实现MVP验证可行性,再逐步重构提升质量。MVP阶段容忍粗糙的错误处理和低效算法,重点是"跑起来"。重构阶段优先解决错误处理和性能瓶颈,最后再打磨输出格式。

踩过的坑:递归中线性查找子目录大小(O(n²))、权限错误未处理导致panic、输出格式在MVP阶段就花太多时间。教训是"先跑通,再优化"。

落地建议:需求定义控制在3-5个核心功能;MVP用最简单的实现,不追求优雅;重构优先解决错误处理;性能优化用benchmark验证效果;CI在项目稳定后再配置。

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值