惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
C
Cisco Blogs
The Hacker News
The Hacker News
T
Threatpost
S
Schneier on Security
K
Kaspersky official blog
Spread Privacy
Spread Privacy
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
NISL@THU
NISL@THU
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google DeepMind News
Google DeepMind News
Security Latest
Security Latest
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
News and Events Feed by Topic
爱范儿
爱范儿
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
Project Zero
Project Zero
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
GbyAI
GbyAI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
Forbes - Security
Forbes - Security
博客园 - 三生石上(FineUI控件)
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News and Events Feed by Topic
V
V2EX
Webroot Blog
Webroot Blog
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
M
MIT News - Artificial intelligence
Scott Helme
Scott Helme
Simon Willison's Weblog
Simon Willison's Weblog
L
LangChain Blog
W
WeLiveSecurity
Cloudbric
Cloudbric

硅上观道・我的个人技术博客

函数式心法 (3):巧用柯里化和部分应用 函数式心法 (2):理解运用高阶函数 打造 NixOS 开发环境 (2):NixOS学习与配置入门指南 打造 NixOS 开发工作流 (1):为什么选择 NixOS Haskell 心法 (1):初识 - 为何学、学什么和怎么学 Clojure 笔记 (2):基本语法结构和 REPL 驱动编程 Clojure 笔记 (1):语言生态介绍和环境配置 前端笔记 (1):ES 标准和 ES6+ 变量定义方法 当我们发现毕生追求的知识在AI面前毫无价值时,如何重建存在的意义? 5 月 28 日 DeepSeek R1 模型完成小版本试升级并开源,具体有哪些提升? 信息闭塞和被信息洪流冲刷,哪个弊端更大?谈错误信息的危害性 Rust CLI 实战:手搓微型 grep (4) 交互式处理错误文件名问题 Rust CLI 实战:手搓微型 grep (2) 实现软件第一版 Rust CLI 实战:手搓微型 grep (1) 新建项目 学计算机有什么好的获取学习资料的方法?掌握高效的资料收集技巧 有哪些看起来很高端的技术其实原理很暴力很初级?浅析CDN内容分发网络原理 学习编程可以为自己带来什么?- 理论与实践的紧密结合 长文 - 大一技术成长复盘:课程、竞赛与开源之旅 推荐一个IDE中的AI工具 - CodeGeex插件和AI辅助编程 关于本站 - 一篇旧文章,我对本站内容更新计划的最初想法 读书笔记:读庄子《齐物论》,探寻真正的自我 读书笔记 - 单向度的人:核心思想,内容解析以及我的感悟 一篇时评写作练习:矛盾与救赎 - 关于行善
Rust CLI 实战:手搓微型 grep (3) 更优雅的错误处理
硅上观道 · 2025-05-18 · via 硅上观道・我的个人技术博客
技术教程

完成了最基本的文本文件搜索功能,下一步就是让项目的错误处理更优雅。欢迎来到我的 Rust 命令行软件开发入门教程第三篇。

完成了最基本的文本文件搜索功能,下一步就是让项目的错误处理更优雅

欢迎来到我的 Rust 命令行软件开发入门教程第三篇,如果你还没看过上篇文章的内容,欢迎查看。

也欢迎关注这个系列专栏,我将更新更多精彩内容。


1. 初版实现代码拆解

上篇文章结束时,我们实现了grrs的基本功能,给出了第一版实现方案:

use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path).expect("could not read file");

    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }
}

我们已经详细讨论过和Cli结构体相关的信息,现在,请大家将注意力集中在main函数上。

在完成参数解析后,我们使用content来储存被查询文件的内容。这个文件读取的过程是怎样的呢?

注意到,我们调用了标准库中的std::fs::read_to_string函数,并且给这个函数传入了&args.path,也就是刚刚我们读取的文件路径。

这里的&符号表示的是引用,因为这里我们无需向函数转移args的所有权。这是rust语言的内存管理机制之一,如果还不是很熟悉的朋友可以再查阅一下官方语言教程。

这里就有个问题:要是用户传入的文件根本不存在,怎么办?显然,这是有可能的。

比如:

初版代码输入错误文件名后的截图

目前这个版本,是在完成这个函数之后链式调用了另一个函数:.expect()。这个函数的作用是,如果出现了无法读取的路径,立刻崩溃退出程序。显然,这不算是一种足够优雅的错误处理办法。


2. 理解Result和模式匹配

为能够更好实现错误处理,我们必须理解read_to_string到底返回了什么。实际上,返回的是一个Result<String, Error>结构。

Result<T, E>rust语言中一种用于辅助错误处理的独特枚举结构,返回的结果既可以是包含成功值的Ok(T),也可以是包含错误信息的Err(E)

在这里,我们可以使用模式匹配来完成对这个函数结果的处理,比如:

let result = std::fs::read_to_string("nonexistent.txt");
match result {
    Ok(content) => { println!("File content: {}", content); }
    Err(error) => { println!("Oh noes: {}", error); }
}

就像拆快递一样,模式匹配会拆开Result这个包裹:如果是Ok就取出值,如果是Err就处理错误。如果你之前有过函数式语言的经历,这样的模式匹配语法相信你一定倍感亲切。

利用这种机制,我们可以稍稍改写一下之前的代码。


3. 通过unwrap进行错误处理

rust中,panic!宏可以让程序立刻崩溃并退出。所以,开头我们的代码也可以这么写:

let result = std::fs::read_to_string("nonexistent.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
println!("file content: {}", content);

简单来说,就是如果Ok()那么我们继续,如果不幸报Err()那么我们立即崩溃退出。

实际上,这还有一种简写版代码:

let content = std::fs::read_to_string("nonexistent.txt").unwrap();

我们把所有这个代码整合进本来的main函数:

fn main() {
    let args = Cli::parse();
    let content = std::fs::read_to_string("nonexistent.txt").unwrap();

    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }
}

尝试执行一个不存在的命令,我们会看到:

传入错误路径后程序按预期崩溃

和我们预计的一样,程序在读取文件失败后立即崩溃退出。

事实上,这也就是为什么大家经常看到提醒,生产环境中一定要慎用unwrap()函数

除非我们十分确认程序不会崩溃,或者像目前我们的程序一样,崩溃了不会产生太严重的后果,否则尽量不要使用这个函数,还是深思熟虑一下如何进行错误处理吧。


4. 更优雅的无panic错误处理

你可能会说,那这个unwrap还是立刻崩溃退出,没什么区别啊。别着急,铺垫这么多就是为了最后这一步优化的。

既然直接退出不好,而结果可能成功也可能有报错,那我们为什么不也使用一个Result呢?

请看示例代码:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("nonexistent.txt");
    let content = match result {
        Ok(content) => { content },
        Err(error) => { return Err(error.into()); }
    };
    println!("file content: {}", content);
    Ok(())
}

这里,如果说result结果是报错,我们不会崩溃结束程序,而是返回一个Err(error.into())值。反之,则返回Ok(())

你可能注意到最后的Ok(())前没有用关键字return。事实上,由于rust中所有由大括号包裹的代码块最后一行都是默认返回值,这里我们可以简写忽略return

rust需要开发者对所有有返回值的函数进行详细的类型标注。这里,我们看到返回类型是Result<(), Box<dyn std::error::Error>>

这里的Box<dyn std::error::Error>实际上是一种可以存储任何错误的“盒子”,只要该错误类型实现了Error trait。而main函数中的error.into()实际上在做类型转换,将原本的 Error 转换为我们指定的Box<dyn Error>结构。

如果你不确定这段讨论的细节,没关系,继续往下读,这不影响我们后续的开发。

事实上,和unwrap一样,我们刚刚通过模式匹配完成的错误处理也有一种简写法:

let content = std::fs::read_to_string("nonexistent.txt")?;

对,你没看错,只是加了个问号?就搞定了,这个?就是等同于match模式匹配 +return Err(error.into)的语法糖。是不是很方便呢?


5. 总结

现在,我们将刚刚学会的?上抛错误法整合进项目,你应该得到的完整项目代码是:

use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path)?;

    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }
    Ok(())
}

我们尝试一个可以成功执行的搜索命令:

成功的搜索命令

再尝试一个应当报错的命令:

不存在路径的错误命令,注意这次程序不再崩溃

可以看到,之前一直有的程序崩溃提示,现在就不存在了!在错误处理上,我们向前走了坚实的一步。

rust中,?是传播错误传播的惯用方式。如果你之前有过别的编程语言的经验,你应当发现这种错误处理方式比一般的try...catch...finally甚至if err != nil要方便的多,对,说的就是你,golang

不知道本文所讲的Result类型和?上抛错误的方式,你是否学会了呢?

到这里,我们会发现出错以后返回的 Error 是Debug格式展示的,对于开发者来说没什么问题,但对终端用户不够友好。

下篇文章,我将聊聊如何让命令行应用的输出结果更优美


希望这篇博客能帮助到你,也欢迎关注交流。愿与君共勉!

点击阅读下一篇文章