最小可行的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/)相当有说服力地指出,答案可能是“没有”?
相似文章
别给自己设障:用Tripwire测试Zig中的错误恢复
Mitchell Hashimoto 介绍了 Tripwire,这是一个Zig库,它通过注入失败来测试错误恢复路径,并且在禁用时零运行时开销。
重返Zig
作者描述了从Zig到Rust再回到Zig的历程,探讨了编程语言中稳定性与表达力之间的权衡。
Zig ELF 二进制文件代码高尔夫 (2025)
深入技术探讨如何缩小 Zig ELF 二进制文件的大小,从 2180K 缩减至 500 字节以下,通过去除调试信息、切换到 ReleaseSmall 以及使用 freestanding 目标。
Zig 示例教程
通过带注释的示例,对 Zig 编程语言进行实践性介绍,涵盖从基础到高级的主题。灵感来源于 Go by Example。
在Zig中使用Comptime条件禁用代码
Mitchell Hashimoto解释了如何利用Zig的comptime特性在编译时条件禁用代码,并与C和Go中的实现方式进行了比较。