第13章 文件系统与 I/O
第13章 文件系统与 I/O
无论你是写一个简单的日志工具,还是构建高性能的 Web 服务器,输入/输出 (I/O) 都是绕不开的核心话题。
在 Zig 中,I/O 操作围绕着两个核心接口构建:Reader 和 Writer。这与 Go 或 Rust 等现代语言的设计理念非常相似——通过统一的接口,让你用同一套逻辑处理文件、网络套接字甚至内存缓冲区。
本章我们将深入探讨 Zig 的 I/O 哲学,学习如何高效地读写文件,以及如何操作目录。
1. 核心接口:Reader 与 Writer
抽象的力量
为什么我们需要 Reader 和 Writer 接口?
想象一下,你写了一个函数 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)是昂贵的。每次调用 write 或 read 都可能触发一次系统调用。如果你在一个循环里每次只写 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();
}
黄金法则:在执行大量小块读写操作时,务必使用
BufferedWriter或BufferedReader,并且在写操作结束时记得flush()。
3. 文件操作 (File System)
Zig 通过 std.fs 模块提供跨平台的文件系统操作。核心入口是 std.fs.cwd()(当前工作目录)。
相对路径与绝对路径
出于安全考虑,Zig 的文件操作通常基于一个目录句柄 (Dir)。最常用的是 cwd() (Current Working Directory)。
所有传递给 dir.openFile 或 dir.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});
}
总结
- Reader/Writer:是 Zig I/O 的核心抽象,学会针对接口编程。
- 缓冲:为了性能,记得使用
std.io.bufferedWriter,并且不要忘记flush()。 - cwd():文件操作通常从
std.fs.cwd()开始,使用相对路径。 - 资源管理:打开文件或目录后,永远记得使用
defer close()。
掌握了 I/O,你就具备了与外部世界交互的能力。这为我们下一章的异步编程打下了基础。