Skip to content

std::io

std::io 是 Rust 的输入输出模块, 提供了读取输入、写入输出、错误处理等基础能力

介绍

std::io 中比较重要的内容:

类型/接口作用
stdin标准输入(键盘)
stdout标准输出(屏幕)
stderr标准错误输出
Read读取字节的 trait(文件/网络/缓冲区都实现了)
Write写入字节的 trait
BufReader带缓冲的读取器, 减少系统调用
ErrorIO 错误类型

读取用户输入

最常用的读取输入方式是 read_line, 它是 std::io::stdin() 返回的 Stdin 上的方法

rust
use std::io;

fn main() {
    // read_line: 读取一行用户输入到字符串
    // 注意: 返回的字符串会包含结尾的换行符 \n
    let mut name = String::new();
    io::stdin().read_line(&mut name).expect("读取失败");

    println!("你好, {name}"); // 你好, 张三 (用户输入)
}

经典场景: 模拟简单的命令行问答

rust
use std::io;

fn main() {
    // 循环读取用户输入, 输入 exit 退出
    loop {
        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("读取失败");

        // trim: 去掉首尾空白(包括换行符)
        let input = input.trim();
        if input == "exit" {
            println!("再见!");
            break;
        }
        println!("你输入了: {input}");
    }
}

向标准输出写入

println! 宏已经足够常用, 但是有些场景需要直接使用 Write trait, 比如写入字节数据

rust
use std::io::Write;

fn main() {
    // write_all: 写入字节数据(注意是 b"..." 字节字符串)
    let mut stdout = std::io::stdout();
    stdout.write_all(b"hello io\n").expect("写入失败");
    stdout.flush().expect("刷新失败");

    // eprintln 已经讲过, 这里演示通过 stderr 写入
    let mut stderr = std::io::stderr();
    stderr.write_all(b"error message\n").expect("写入失败");
}

缓冲读取 BufReader

直接读取文件/网络数据时, 每读一次都会发生一次系统调用, 使用 BufReader 可以先读一大块到内存, 再按需取用

rust
use std::io::BufRead;
use std::io::BufReader;

fn main() {
    // BufReader::new: 任意实现了 Read 的类型都可以包一层缓冲
    let data = "第一行\n第二行\n第三行\n";
    let mut reader = BufReader::new(data.as_bytes());

    // read_line: 逐行读取(需要引入 BufRead trait)
    let mut line = String::new();
    reader.read_line(&mut line).expect("读取失败");
    println!("第一行: {line}"); // 第一行: 第一行

    // lines: 返回每行迭代器(已经去掉换行符)
    let data = "a\nb\nc\n";
    let reader = BufReader::new(data.as_bytes());
    for line in reader.lines() {
        let line = line.expect("读取失败");
        println!("{line}"); // a / b / c
    }
}

std::fs

std::fs 是 Rust 的文件系统模块, 提供了读写文件、操作目录等能力

读取文件

方法作用
fs::read_to_string读取整个文件为字符串
fs::read读取整个文件为字节数据
File::open打开文件(只读), 再配合 Read trait 读取
rust
use std::fs;
use std::io::Read;

fn main() {
    // 先创建文件(示例自包含, 写读闭环)
    let path = std::env::temp_dir().join("rust_read_demo.txt");
    fs::write(&path, "hello 文件系统\n第二行").expect("写入失败");

    // 1. read_to_string: 读取整个文件为字符串
    let content = fs::read_to_string(&path).expect("读取失败");
    println!("{content}"); // hello 文件系统 / 第二行

    // 2. read: 读取整个文件为字节数据
    let bytes = fs::read(&path).expect("读取失败");
    println!("bytes len = {}", bytes.len());

    // 3. File::open + read_to_string: 分步读取
    let mut file = fs::File::open(&path).expect("打开失败");
    let mut content = String::new();
    file.read_to_string(&mut content).expect("读取失败");
    println!("{content}");

    // 清理临时文件
    fs::remove_file(&path).expect("删除失败");
}

写入文件

方法作用
fs::write一步完成"创建+写入"
File::create创建文件, 再配合 Write trait 写入
rust
use std::fs;
use std::io::Write;

fn main() {
    let path = std::env::temp_dir().join("rust_write_demo.txt");

    // 1. fs::write: 一步完成创建+写入
    fs::write(&path, "第一次写入").expect("写入失败");
    println!("{}", fs::read_to_string(&path).expect("读取失败")); // 第一次写入

    // 2. File::create + write_all: 分步写入
    // 注意: b"..." 是字节字符串, 只能包含 ASCII 字符, 中文要用 .as_bytes()
    let mut file = fs::File::create(&path).expect("创建失败");
    file.write_all("第二次写入".as_bytes()).expect("写入失败");
    println!("{}", fs::read_to_string(&path).expect("读取失败")); // 第二次写入

    fs::remove_file(&path).expect("删除失败");
}

注意点

fs::writeFile::create 都是覆盖式写入: 文件已存在时会清空原内容, 需要追加内容请使用 OpenOptions

追加写入

使用 OpenOptions 可以灵活控制打开方式, 比如追加模式

rust
use std::fs::OpenOptions;
use std::io::Write;

fn main() {
    let path = std::env::temp_dir().join("rust_append_demo.txt");

    // append(true): 追加模式
    // create(true): 文件不存在时自动创建
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open(&path)
        .expect("打开失败");

    // 注意: b"..." 字节字符串只能包含 ASCII, 中文用 .as_bytes()
    file.write_all("第1行\n".as_bytes()).expect("写入失败");
    file.write_all("第2行\n".as_bytes()).expect("写入失败");

    let content = std::fs::read_to_string(&path).expect("读取失败");
    println!("{content}"); // 第1行 / 第2行

    std::fs::remove_file(&path).expect("删除失败");
}

目录操作

方法作用
create_dir创建目录(父目录不存在会报错)
create_dir_all递归创建目录(父目录不存在会自动创建)
read_dir遍历目录, 返回目录条目迭代器
remove_dir删除空目录
remove_dir_all递归删除目录(不管里面有没有内容)
rust
use std::fs;

fn main() {
    let base = std::env::temp_dir().join("rust_dir_demo");

    // create_dir: 创建目录
    fs::create_dir(&base).expect("创建目录失败");
    println!("create_dir 成功");

    // create_dir_all: 递归创建(中间目录不存在也会创建)
    let nested = base.join("a/b/c");
    fs::create_dir_all(&nested).expect("递归创建失败");
    println!("create_dir_all 成功");

    // 在目录里放一个文件, 用于演示 read_dir
    fs::write(base.join("a/hello.txt"), "hi").expect("写入失败");

    // read_dir: 遍历目录(条目顺序不确定)
    for entry in fs::read_dir(base.join("a")).expect("读取目录失败") {
        let entry = entry.expect("读取条目失败");
        println!("{}", entry.file_name().to_string_lossy()); // b / hello.txt (顺序不确定)
    }

    // remove_dir: 只能删除空目录
    fs::remove_dir(base.join("a/b/c")).expect("删除目录失败");
    println!("remove_dir 成功");

    // remove_dir_all: 递归删除(目录非空也可以删)
    fs::remove_dir_all(&base).expect("递归删除失败");
    println!("remove_dir_all 成功");
}

文件操作

方法作用
remove_file删除文件
rename重命名/移动文件
copy复制文件, 返回复制的字节数
exists判断路径是否存在
rust
use std::fs;

fn main() {
    let base = std::env::temp_dir().join("rust_file_ops");
    fs::create_dir_all(&base).expect("创建目录失败");

    // 准备源文件
    let src = base.join("src.txt");
    fs::write(&src, "hello").expect("写入失败");

    // copy: 复制文件, 返回复制的字节数
    let dst = base.join("dst.txt");
    let copied = fs::copy(&src, &dst).expect("复制失败");
    println!("copied = {copied} 字节"); // copied = 5 字节

    // rename: 重命名(也可以用来移动文件)
    let renamed = base.join("renamed.txt");
    fs::rename(&dst, &renamed).expect("重命名失败");

    // exists: 判断路径是否存在
    println!("renamed 存在 = {}", renamed.exists()); // renamed 存在 = true
    println!("dst 存在 = {}", dst.exists()); // dst 存在 = false

    // remove_file: 删除文件
    fs::remove_file(&renamed).expect("删除失败");
    println!("删除后存在 = {}", renamed.exists()); // 删除后存在 = false

    fs::remove_dir_all(&base).expect("清理失败");
}

文件信息

使用 metadata 获取文件的元信息

方法作用
metadata获取文件/目录的元信息
len文件大小(字节)
is_file是否是文件
is_dir是否是目录
rust
use std::fs;

fn main() {
    let path = std::env::temp_dir().join("rust_meta_demo.txt");
    fs::write(&path, "hello").expect("写入失败");

    // metadata: 获取元信息
    let meta = fs::metadata(&path).expect("获取元信息失败");

    println!("len = {}", meta.len()); // len = 5
    println!("is_file = {}", meta.is_file()); // is_file = true
    println!("is_dir = {}", meta.is_dir()); // is_dir = false
    println!("readonly = {}", meta.permissions().readonly()); // readonly = false

    // 目录的元信息
    let dir_meta = fs::metadata(std::env::temp_dir()).expect("获取元信息失败");
    println!("temp_dir is_dir = {}", dir_meta.is_dir()); // temp_dir is_dir = true

    fs::remove_file(&path).expect("删除失败");
}

错误处理与 ? 运算符

文件操作几乎都会返回 Result, 使用 ? 运算符可以方便地把错误向上传播

rust
use std::fs;

fn main() {
    let path = std::env::temp_dir().join("rust_question.txt");
    fs::write(&path, "42").expect("写入失败");

    // 读取成功
    match read_number(&path) {
        Ok(n) => println!("数字 = {n}"), // 数字 = 42
        Err(e) => println!("出错: {e}"),
    }

    // 文件不存在, ? 会把错误传播给调用方
    match read_number(&std::env::temp_dir().join("no_such_file.txt")) {
        Ok(n) => println!("数字 = {n}"),
        Err(e) => println!("出错: {e}"), // 出错: No such file or directory (os error 2)
    }

    fs::remove_file(&path).expect("删除失败");
}

// ? 运算符: 读取失败/解析失败都会自动返回 Err
fn read_number(path: &std::path::Path) -> Result<i32, std::io::Error> {
    let content = fs::read_to_string(path)?;
    // 注意: parse 返回的错误类型是 ParseIntError, 不是 io::Error
    // 需要用 map_err 转换成 io::Error 才能用 ? 传播
    let n = content
        .trim()
        .parse::<i32>()
        .map_err(|_| std::io::Error::other("内容不是数字"))?;
    Ok(n)
}

main 函数也可以返回 Result, 出错时程序会打印错误并以非 0 状态码退出

rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 使用 Box<dyn Error> 统一错误类型
    // 读取文件(io::Error) 和 解析数字(ParseIntError) 是不同的错误类型,
    // 但它们都实现了 std::error::Error, 所以都可以直接用 ? 传播
    let path = std::env::temp_dir().join("rust_box_demo.txt");
    std::fs::write(&path, "42").expect("写入失败");

    let num: i32 = std::fs::read_to_string(&path)?.trim().parse()?;
    println!("num = {num}"); // num = 42

    std::fs::remove_file(&path).expect("删除失败");
    Ok(())
}

经典场景: 统计文件行数

rust
use std::fs;
use std::io::BufRead;

fn main() {
    let path = std::env::temp_dir().join("rust_lines_demo.txt");
    fs::write(&path, "第一行\n第二行\n第三行\n").expect("写入失败");

    // BufReader + lines: 逐行读取
    let file = fs::File::open(&path).expect("打开失败");
    let reader = std::io::BufReader::new(file);

    let mut count = 0;
    for line in reader.lines() {
        let line = line.expect("读取失败");
        println!("第{}行: {line}", count + 1); // 第1行: 第一行 / 第2行: 第二行 / 第3行: 第三行
        count += 1;
    }
    println!("总共 {count} 行"); // 总共 3 行

    fs::remove_file(&path).expect("删除失败");
}

std::path

std::path 模块提供了跨平台的路径类型: PathPathBuf

Path 与 PathBuf 的区别

PathPathBuf 的关系, 和 &strString 的关系一模一样:

类型是否拥有数据类似关系
Path借用(不拥有)类似 &str
PathBuf拥有(可以修改)类似 String
rust
use std::path::{Path, PathBuf};

fn main() {
    // Path: 借用已有的路径字符串
    let p: &Path = Path::new("/tmp/hello.txt");
    println!("{p:?}"); // "/tmp/hello.txt"

    // PathBuf: 拥有自己的数据, 可以修改
    let mut pb: PathBuf = PathBuf::from("/tmp");
    pb.push("hello.txt");
    println!("{pb:?}"); // "/tmp/hello.txt"

    // PathBuf 可以自动借用为 Path
    let p2: &Path = &pb;
    println!("{p2:?}"); // "/tmp/hello.txt"
}

创建路径

方法作用
Path::new从字符串创建 Path(借用)
PathBuf::from从字符串创建 PathBuf(拥有)
push追加路径段(修改自身)
join拼接路径(不修改自身, 返回新路径)
rust
use std::path::PathBuf;

fn main() {
    // 1. PathBuf::from: 从字符串创建
    let mut base = PathBuf::from("/home/user");
    println!("{base:?}"); // "/home/user"

    // 2. push: 追加路径段(会修改自身)
    base.push("projects");
    base.push("rust_demo");
    println!("{base:?}"); // "/home/user/projects/rust_demo"

    // 3. join: 不修改自身, 返回拼接后的新路径
    let full = base.join("src").join("main.rs");
    println!("{full:?}"); // "/home/user/projects/rust_demo/src/main.rs"
    println!("{base:?}"); // "/home/user/projects/rust_demo" (base 没有变)
}

路径常用方法

方法作用
file_name文件名(最后一段)
extension文件扩展名
parent父路径
is_absolute是否是绝对路径
exists路径是否存在
rust
use std::path::Path;

fn main() {
    let path = Path::new("/home/user/projects/main.rs");

    println!("file_name = {:?}", path.file_name()); // file_name = Some("main.rs")
    println!("extension = {:?}", path.extension()); // extension = Some("rs")
    println!("parent = {:?}", path.parent()); // parent = Some("/home/user/projects")
    println!("is_absolute = {}", path.is_absolute()); // is_absolute = true

    let rel = Path::new("docs/readme.md");
    println!("is_absolute = {}", rel.is_absolute()); // is_absolute = false
    println!("starts_with docs = {}", rel.starts_with("docs")); // starts_with docs = true

    // exists: 判断路径是否存在
    println!("exists = {}", path.exists()); // exists = false
}

经典场景: 遍历目录

rust
use std::fs;
use std::path::Path;

fn main() {
    // 准备测试目录
    let base = std::env::temp_dir().join("rust_walk_demo");
    fs::create_dir_all(base.join("sub")).expect("创建失败");
    fs::write(base.join("a.txt"), "a").expect("写入失败");
    fs::write(base.join("sub/b.txt"), "b").expect("写入失败");

    // 遍历一层目录, 区分文件和目录
    println!("=== 第一层 ===");
    for entry in fs::read_dir(&base).expect("读取目录失败") {
        let entry = entry.expect("读取条目失败");
        let path = entry.path();
        if path.is_dir() {
            println!("[目录] {}", path.file_name().unwrap().to_string_lossy());
        } else {
            println!("[文件] {}", path.file_name().unwrap().to_string_lossy());
        }
    }

    // 递归遍历所有层
    println!("=== 递归遍历 ===");
    walk(&base);

    fs::remove_dir_all(&base).expect("清理失败");
}

fn walk(dir: &Path) {
    for entry in fs::read_dir(dir).expect("读取目录失败") {
        let entry = entry.expect("读取条目失败");
        let path = entry.path();
        if path.is_dir() {
            walk(&path); // 递归进入子目录
        } else {
            println!("{}", path.display());
        }
    }
}

相关开源库

标准库的文件操作已经够用, 但以下库可以简化一些复杂场景:

  • walkdir: 递归遍历目录, 比手写递归更安全
  • tempfile: 创建自动清理的临时文件/目录
rust
// Cargo.toml 添加依赖:
// [dependencies]
// walkdir = "2"

fn main() {
    // walkdir: 一行代码递归遍历目录
    for entry in walkdir::WalkDir::new(".")
        .into_iter()
        .filter_map(|e| e.ok())
    {
        println!("{}", entry.path().display());
    }
}
rust
// Cargo.toml 添加依赖:
// [dependencies]
// tempfile = "3"

fn main() {
    // NamedTempFile: 创建临时文件, 程序结束时自动删除
    let file = tempfile::NamedTempFile::new().expect("创建临时文件失败");
    println!("临时文件: {}", file.path().display());

    // 程序退出时, 临时文件会被自动清理
}

Released under the MIT License.