第13章 文件系统与 I/O

第13章 文件系统与 I/O

无论你是写一个简单的日志工具,还是构建高性能的 Web 服务器,输入/输出 (I/O) 都是绕不开的核心话题。

在 Zig 中,I/O 操作围绕着两个核心接口构建:ReaderWriter。这与 Go 或 Rust 等现代语言的设计理念非常相似——通过统一的接口,让你用同一套逻辑处理文件、网络套接字甚至内存缓冲区。

本章我们将深入探讨 Zig 的 I/O 哲学,学习如何高效地读写文件,以及如何操作目录。

1. 核心接口:Reader 与 Writer

抽象的力量

为什么我们需要 ReaderWriter 接口? 想象一下,你写了一个函数 saveUser。如果它只能接受一个文件路径,那你怎么测试它?怎么把用户数据保存到网络数据库?

如果 saveUser 接受的是一个通用的 Writer,那么:

  • 传给它一个文件 Writer -> 保存到文件。
  • 传给它一个 Socket Writer -> 发送到网络。
  • 传给它一个 ArrayList Writer -> 保存到内存(方便测试)。

这就是抽象的力量。

标准流 (Stdio)

最简单的 I/O 就是标准输入输出。我们之前的章节已经多次用到了。

const std = @import("std");

pub fn main() !void {
    // 获取标准输出的 writer
    const stdout = std.io.getStdOut().writer();
    
    // 获取标准输入的 reader
    const stdin = std.io.getStdIn().reader();

    try stdout.print("Hello, I/O!\n", .{});
}

常用 Writer 方法

  • print(fmt, args):格式化打印。
  • writeAll(bytes):写入所有字节。
  • writeByte(u8):写入单个字节。

常用 Reader 方法

  • readAll(buffer):尝试填满缓冲区。
  • readUntilDelimiter(buffer, delimiter):读取直到遇到分隔符(如换行符)。
  • readByte():读取单个字节。

2. 缓冲 I/O (Buffered I/O)

这是新手最容易踩的坑之一:性能问题。

系统调用(System Call)是昂贵的。每次调用 writeread 都可能触发一次系统调用。如果你在一个循环里每次只写 1 个字节,性能会非常糟糕。

解决方案:使用 缓冲 (Buffer)

缓冲 I/O 会在内存中积攒一批数据,积攒够了(比如 4KB)才一次性调用系统接口写入磁盘。

Zig 中的缓冲

Zig 默认不缓冲标准流(除了 std.debug.print)。你需要显式地包装你的 Reader/Writer。

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().createFile("output.txt", .{});
    defer file.close();

    // 1. 创建无缓冲的 writer (直接对文件操作)
    // const writer = file.writer(); 

    // 2. 创建带缓冲的 writer (推荐)
    // BufferedWriter 需要传入下层 writer 的类型
    var bw = std.io.bufferedWriter(file.writer());
    
    // 获取 bw 的 writer 接口
    const writer = bw.writer();

    try writer.print("This is buffered!\n", .{});
    
    // 3. 关键:刷新缓冲区!
    // 如果忘记 flush,留在缓冲区的数据可能不会被写入文件
    try bw.flush();
}

黄金法则:在执行大量小块读写操作时,务必使用 BufferedWriterBufferedReader,并且在写操作结束时记得 flush()

3. 文件操作 (File System)

Zig 通过 std.fs 模块提供跨平台的文件系统操作。核心入口是 std.fs.cwd()(当前工作目录)。

相对路径与绝对路径

出于安全考虑,Zig 的文件操作通常基于一个目录句柄 (Dir)。最常用的是 cwd() (Current Working Directory)。

所有传递给 dir.openFiledir.createFile 的路径,通常都是相对路径。绝对路径也是支持的,但 Zig 鼓励使用基于目录句柄的操作,这在防止路径遍历攻击(Path Traversal)时更安全。

创建与写入文件

const std = @import("std");

pub fn main() !void {
    const cwd = std.fs.cwd();

    // 创建文件 (如果存在则覆盖)
    // .read = true 表示创建后我们还要读取它
    const file = try cwd.createFile("data.txt", .{ .read = true });
    defer file.close();

    try file.writeAll("Hello Zig Files!\n");
    
    // 移动文件指针到开头 (因为写入后指针在末尾)
    try file.seekTo(0);

    var buffer: [100]u8 = undefined;
    const bytes_read = try file.readAll(&buffer);
    std.debug.print("Read back: {s}\n", .{buffer[0..bytes_read]});
}

打开与读取文件

pub fn readFile() !void {
    const cwd = std.fs.cwd();

    // 打开文件
    // mode 默认为 .read_only
    const file = try cwd.openFile("data.txt", .{});
    defer file.close();

    // 限制读取大小,防止文件过大撑爆内存
    const max_bytes = 1024 * 1024; // 1MB
    // 使用分配器读取整个文件内容
    const content = try file.readToEndAlloc(std.heap.page_allocator, max_bytes);
    defer std.heap.page_allocator.free(content);

    std.debug.print("File content: {s}\n", .{content});
}

常用文件操作

  • cwd.makeDir("subdir"):创建目录。
  • cwd.makePath("a/b/c"):递归创建目录(类似 mkdir -p)。
  • cwd.deleteFile("file.txt"):删除文件。
  • cwd.deleteDir("subdir"):删除空目录。
  • cwd.rename("old", "new"):重命名/移动文件。
  • cwd.statFile("file"):获取文件信息(大小、修改时间等)。

4. 遍历目录

遍历目录下的文件也是常见需求。Zig 提供了迭代器模式。

pub fn listDir() !void {
    var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var iterator = dir.iterate();
    while (try iterator.next()) |entry| {
        // entry.name 是文件名
        // entry.kind 是文件类型 (file, directory, etc.)
        std.debug.print("{s} ({s})\n", .{ entry.name, @tagName(entry.kind) });
    }
}

5. JSON 处理 (序列化/反序列化)

虽然 JSON 不完全属于 I/O,但它们经常一起出现(读文件 -> 解析 JSON)。Zig 标准库内置了高性能的 JSON 支持。

const User = struct {
    name: []const u8,
    age: u8,
};

pub fn jsonDemo() !void {
    const allocator = std.heap.page_allocator;

    // 1. 序列化 (Stringify)
    const user = User{ .name = "Ziggy", .age = 10 };
    var string = std.ArrayList(u8).init(allocator);
    defer string.deinit();
    
    try std.json.stringify(user, .{}, string.writer());
    std.debug.print("JSON: {s}\n", .{string.items});

    // 2. 反序列化 (Parse)
    const json_text = "{\"name\": \"Alice\", \"age\": 25}";
    const parsed = try std.json.parseFromSlice(User, allocator, json_text, .{});
    defer parsed.deinit(); // 释放解析过程中分配的内存(如字符串)

    std.debug.print("Parsed User: {s}, {d}\n", .{parsed.value.name, parsed.value.age});
}

总结

  1. Reader/Writer:是 Zig I/O 的核心抽象,学会针对接口编程。
  2. 缓冲:为了性能,记得使用 std.io.bufferedWriter,并且不要忘记 flush()
  3. cwd():文件操作通常从 std.fs.cwd() 开始,使用相对路径。
  4. 资源管理:打开文件或目录后,永远记得使用 defer close()

掌握了 I/O,你就具备了与外部世界交互的能力。这为我们下一章的异步编程打下了基础。


原文出处: https://pedropark99.github.io/zig-book/