最小可行的Zig错误上下文
摘要
一篇博文,详细介绍了使用errdefer日志在Zig中添加错误上下文的最小模式,并将其与完整的诊断接收器(diagnostics sinks)和catch块进行比较,讨论了权衡取舍。
<header>
<h1>最小可行的Zig错误上下文</h1>
<time class="meta" datetime="2026-05-03">May 3, 2026</time>
</header>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">fn</span><span class="hl-function"> process_file</span>(io: Io, path: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>) <span class="hl-operator">!</span><span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-keyword">errdefer</span> log.err(<span class="hl-string">"path={s}"</span>, .{path});</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">const</span> fd = <span class="hl-keyword">try</span> Io.Dir.cwd().openFile(io, path, .{});</span>
<span class="line"> <span class="hl-keyword">defer</span> fd.close(io);</span>
<span class="line"></span>
<span class="line"> <span class="hl-comment">// ...</span></span>
<span class="line">}</span></code></pre>
</figure>
<p>开箱即用,Zig提供了最简且充分的错误<em>处理</em>设施——
<a href="https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html">强类型错误码</a>。
错误<em>报告</em>则留给用户自己处理。惯用做法是传递一个<code>Diagnostics</code>输出参数
(“接收器”),根据需要生成人类可读的字符串。</p>
<p>Diagnostics模式在“生产”代码中表现良好,但对于偏脚本式的代码,相对于默认的简单
<span class="display"><code>try fallible()</code>,</span>
它增加了太多阻力,而后者在失败时给出的信息显然不理想:</p>
<figure class="code-block">
<pre><code><span class="line">λ zig build</span>
<span class="line">error: FileNotFound</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)</span>
<span class="line"> .NOENT => return error.FileNotFound,</span>
<span class="line"> ^</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)</span>
<span class="line"> return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:10:16: 0x10443da5f in f (fail)</span>
<span class="line"> const fd = try Io.Dir.cwd().openFile(io, path, .{});</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:6:5: 0x10443db47 in main (fail)</span>
<span class="line"> try process_file(io, "data.txt");</span>
<span class="line"> ^</span></code></pre>
</figure>
<p>错误追踪很有帮助,但知道<em>哪个</em>文件出了问题则更是如此。</p>
<p>在成熟的诊断接收器模式和简单的try之间寻找中间地带的第一次尝试大致如下:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">const</span> fd = dir.openFile(io, path, .{}) <span class="hl-keyword">catch</span> <span class="hl-operator">|</span>err<span class="hl-operator">|</span> {</span>
<span class="line"> log.err(<span class="hl-string">"failed to open file '{s}': {t}"</span>, .{path, err});</span>
<span class="line"> <span class="hl-keyword">return</span> err;</span>
<span class="line">}</span></code></pre>
</figure>
<p>并不理想。阻力很大,你需要编造一个听起来合理的错误消息,
代码的“快乐路径”被遮蔽,而且每个可能失败的操作都得重复一遍。</p>
<p>上述代码的一个“更差即更好”的版本是</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">errdefer</span> log.err(<span class="hl-string">"path={s}"</span>, .{path});</span>
<span class="line"><span class="hl-keyword">const</span> fd = <span class="hl-keyword">try</span> dir.openFile(io, path, .{});</span></code></pre>
</figure>
<p>即,只需用<code>errdefer</code>保护,将错误上下文记录为<code>key=value</code>对。结果虽不美观,但尚可接受:</p>
<figure class="code-block">
<pre><code><span class="line">λ zig build</span>
<span class="line">error: path=./data.txt</span>
<span class="line">error: FileNotFound</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)</span>
<span class="line"> .NOENT => return error.FileNotFound,</span>
<span class="line"> ^</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)</span>
<span class="line"> return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:10:16: 0x10443da5f in f (fail)</span>
<span class="line"> const fd = try Io.Dir.cwd().openFile(io, path, .{});</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:6:5: 0x10443db47 in main (fail)</span>
<span class="line"> try process_file(io, "data.txt");</span>
<span class="line"> ^</span></code></pre>
</figure>
<p>阻力大大减少:</p>
<ul>
<li>
无需编造除现有变量名以外的任何错误消息。
</li>
<li>
无需更改任何<code>try</code>语句。
</li>
<li>
上下文按块设置。如果一个函数对一个文件执行多次可能失败的操作,路径只需指定一次。
</li>
<li>
上下文是“望远镜式”的,调用栈中的每个函数都可以添加自己的上下文。
</li>
</ul>
<p>但有一个巨大的缺点——即使后续处理了错误,错误消息也会被记录。这在Zig 0.16中尤其重要,因为取消
(<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1677r2.pdf">serendipitous-success</a>)
可能是任何IO操作可能出现的错误,而它本应被处理,而非报告。</p>
<hr>
<p>概括来说:</p>
<ul>
<li>
快乐路径为所有进行中的操作添加上下文。
</li>
<li>
错误则物化当前上下文。
</li>
</ul>
<p>这确实感觉是一种比在错误发生时单独装饰错误更好的错误管理策略。我想知道哪些语言特性有助于实现这种风格?</p>
<p>这篇文章
<a href="https://goldstein.lol/posts/error-progress/" class="display url">https://goldstein.lol/posts/error-progress/</a>
颇有说服力地论证了答案可能是“没有”?</p>
查看缓存全文
缓存时间:
2026/05/16 03:33
# 最小可行 Zig 错误上下文
来源:https://matklad.github.io/2026/05/03/zig-error-context.html
2026 年 5 月 3 日
``zig
fn process_file(io: Io, path: []const u8) !void {
errdefer log.err("path={s}", .{path});
const fd = try Io.Dir.cwd().openFile(io, path, .{});
defer fd.close(io);
// ...
}
``
开箱即用,Zig 为错误*处理*提供了最小且足够的工具——强类型错误码(https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html)。错误*报告*则留给用户自行实现。惯用的做法是传入一个 `Diagnostics` 输出参数(“sink”),以便在需要时生成人类可读的字符串。
Diagnostics 模式对“生产级”代码来说效果不错,但对于更接近脚本的代码,它带来的开销相对于简单的 `try fallible()` 默认选项来说太大了——后者在失败时给出的信息当然不太理想:
``
λ zig build
error: FileNotFound
~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)
.NOENT => return error.FileNotFound,
^
~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)
return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
^
~/fail/main.zig:10:16: 0x10443da5f in f (fail)
const fd = try Io.Dir.cwd().openFile(io, path, .{});
^
~/fail/main.zig:6:5: 0x10443db47 in main (fail)
try process_file(io, "data.txt");
^
``
错误追踪很有用,但知道*哪个*文件出了问题更为关键。
在完整的 diagnostics sink 模式和简单的 try 之间寻找中间地带的首次尝试大致如下:
``zig
const fd = dir.openFile(io, path, .{}) catch |err| {
log.err("failed to open file '{s}': {t}", .{path, err});
return err;
}
``
并不令人满意。开销仍然很高:你需要想出一条听起来合理的错误消息,“快乐路径”的代码被掩盖,而且每个可能失败的操作都得重复这一套。
以上代码的一个“更烂即更好”的版本是:
``zig
errdefer log.err("path={s}", .{path});
const fd = try dir.openFile(io, path, .{});
``
也就是说,仅仅是用 `errdefer` 守卫,以 `key=value` 对的形式记录错误上下文。结果谈不上漂亮,但还算过得去:
``
λ zig build
error: path=./data.txt
error: FileNotFound
~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)
.NOENT => return error.FileNotFound,
^
~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)
return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
^
~/fail/main.zig:10:16: 0x10443da5f in f (fail)
const fd = try Io.Dir.cwd().openFile(io, path, .{});
^
~/fail/main.zig:6:5: 0x10443db47 in main (fail)
try process_file(io, "data.txt");
^
``
开销大大降低:
- 无需想任何错误消息,直接用已有的变量名即可。
- 无需修改任何 `try` 语句。
- 上下文是按块设置的。如果一个函数对某个文件执行多次可能失败的操作,路径只需要指定一次。
- 上下文是“望远镜式”的——调用栈中的每个函数都可以添加自己的上下文。
不过有一个巨大的缺点:即使用后续处理了错误,错误消息也仍然会被记录。这一点在 Zig 0.16 中尤其重要,因为取消(意外成功(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1677r2.pdf))是任何 IO 操作都可能出现的错误,而它本应被处理,而非被报告。
---
概括来说:
- 快乐路径为所有进行中的操作添加上下文。
- 错误发生时,将当前的上下文具体化。
这感觉上比在错误发生时逐条装饰错误的策略更好。我想知道哪些语言特性有助于实现这种风格?
这篇文章(https://goldstein.lol/posts/error-progress/)相当有说服力地指出,答案可能是“没有”?
相似文章
Mitchell Hashimoto
Mitchell Hashimoto 介绍了 Tripwire,这是一个Zig库,它通过注入失败来测试错误恢复路径,并且在禁用时零运行时开销。
Mitchell Hashimoto
Mitchell Hashimoto解释了如何利用Zig的comptime特性在编译时条件禁用代码,并与C和Go中的实现方式进行了比较。
Lobsters Hottest
# 两个让 `zig fmt` 更好用的技巧
Zig 配备了一个内置的代码格式化工具 `zig fmt`。与其他语言的格式化工具不同,`zig fmt` 是"可操控的"——某些语法结构会影响格式化的输出结果。本文将介绍两个实用技巧。
## 技巧一:尾随逗号控制布局
`zig fmt` 会根据是否存在尾随逗号来决定参数的排列方式。
**没有尾随逗号**时,格式化工具会尝试将所有参数放在同一行:
```zig
const result = myFunction(argument1, argument2, argument3);
```
**有尾随逗号**时,格式化工具会将每个参数单独放在一行:
```zig
const result = myFunction(
argument1,
argument2,
argument3,
);
```
这个规则同样适用于函数定义的参数列表、结构体字段、枚举变体等场景。
```zig
// 单行:无尾随逗号
const Point = struct { x: f32, y: f32 };
// 多行:有尾随逗号
const Point = struct {
x: f32,
y: f32,
};
```
这意味着你可以通过添加或删除尾随逗号来主动控制格式化的输出,而不必与格式化工具"博弈"。想要多行展示?加上尾随逗号。想要单行展示?去掉它。
同样的逻辑也适用于换行符。如果你在参数之间手动添加了换行符,`zig fmt` 会尊重这个选择并保留多行格式——前提是同时带有尾随逗号。
## 技巧二:数组的列式格式化
对于数值数组,`zig fmt` 支持一种特殊的列式格式化方式,非常适合用来表示矩阵或表格数据。
只需在数组元素之间手动插入换行符,`zig fmt` 就会将数据对齐成整洁的列式布局:
```zig
// 格式化前(你写的)
const matrix = [_]f32{
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
};
```
```zig
// 格式化后(zig fmt 输出)
const matrix = [_]f32{
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
};
```
`zig fmt` 会识别出你在每行放置了相同数量的元素,并将各列对齐,使代码更具可读性。这对于表示变换矩阵、查找表或任何具有内在行列结构的数据来说极为方便。
```zig
// 一个更直观的例子:查找表
const sine_table = [_]f32{
0.000, 0.174, 0.342, 0.500,
0.643, 0.766, 0.866, 0.940,
0.985, 1.000, 0.985, 0.940,
0.866, 0.766, 0.643, 0.500,
};
```
## 小结
`zig fmt` 的"可操控"设计哲学让格式化工具成为你的合作伙伴,而不是独裁者:
- **尾随逗号** → 强制多行展开
- **无尾随逗号** → 允许单行折叠
- **手动换行 + 统一列数** → 触发列式对齐
掌握这两个技巧,你就能在享受自动格式化便利的同时,保留对代码视觉呈现的精确控制。
Simon Willison's Blog
Zig 0.16.0 发布了一个名为 'Juicy Main' 的新特性,它为 main() 函数提供了依赖注入功能,方便地访问分配器、IO、环境变量和命令行参数。
Mitchell Hashimoto
Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。