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

推荐订阅源

Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
D
Docker
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Cloudflare Blog
雷峰网
雷峰网
A
About on SuperTechFans
小众软件
小众软件
博客园 - Franky
博客园 - 聂微东
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
量子位
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
罗磊的独立博客
Martin Fowler
Martin Fowler
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
Project Zero
Project Zero
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
S
Schneier on Security
Blog — PlanetScale
Blog — PlanetScale
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Security Latest
Security Latest
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks

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

函数式心法 (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格式展示的,对于开发者来说没什么问题,但对终端用户不够友好。

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


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

点击阅读下一篇文章