最小可行的Zig错误上下文

matklad 工具

摘要

一篇博文,详细介绍了使用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">&quot;path={s}&quot;</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 =&gt; 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, &quot;data.txt&quot;);</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">&quot;failed to open file &#x27;{s}&#x27;: {t}&quot;</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">&quot;path={s}&quot;</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 =&gt; 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, &quot;data.txt&quot;);</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/)相当有说服力地指出,答案可能是“没有”?

相似文章

# 引导 Zig Fmt 在过去的几个月里,我一直在研究 Zig 格式化工具(`zig fmt`)的演变方向,最终形成了[这份提案](https://github.com/ziglang/zig/issues/20078)。由于这是一个颇具争议的话题,我想在这篇文章中详细阐述其中的权衡考量。 ## 现状 `zig fmt` 是一个固执己见的格式化工具:它会将 Zig 代码格式化为单一的规范形式,且不受用户配置的影响。这类工具(如 `gofmt`、`prettier`)有一个显著优势:当整个生态系统都使用同一格式化工具时,就能消除围绕风格的无谓争论,并确保所有代码的外观一致。 然而,现有的 `zig fmt` 存在一个问题:它实际上并不是完全固执己见的——格式化结果会根据用户的输入而变化。 以下面这个例子为例: ```zig const x = foo(1, 2, 3); ``` 如果你在最后一个参数后面加上一个逗号: ```zig const x = foo(1, 2, 3,); ``` `zig fmt` 会将其格式化为: ```zig const x = foo( 1, 2, 3, ); ``` 这意味着代码的格式化方式(单行还是多行)取决于用户是否添加了尾随逗号。此外,`zig fmt` 对于其他一些构造也并非完全固执己见,例如注释的位置。 ## 问题所在 为什么这是个问题呢?毕竟,根据尾随逗号来决定格式化方式,这是一种广为人知且合理的约定。 问题在于,这种方案实际上给了用户两种选择:单行格式和多行格式。这意味着用户必须做出决定,而这恰恰是格式化工具本应消除的那类决策。 更糟糕的是,这个决定并没有一个客观正确的答案,因为适合单行还是多行,往往取决于行的长度——而行的长度会随着变量名、参数等的变化而改变。 举个例子,假设你有: ```zig const x = foo(1, 2, 3); ``` 这段代码很短,放在一行完全没问题。但如果函数名变长了呢? ```zig const x = a_longer_function_name(1, 2, 3); ``` 还是挺短的。那如果更长呢? ```zig const x = a_much_much_longer_function_name(argument_one, argument_two, argument_three); ``` 这行已经相当长了,或许应该换成多行格式: ```zig const x = a_much_much_longer_function_name( argument_one, argument_two, argument_three, ); ``` 但在当前的 `zig fmt` 机制下,你需要手动添加尾随逗号来触发这个格式化。如果你重构代码,将函数名改短,或者把参数替换为更短的名称,那么多行格式可能就不再必要了——但 `zig fmt` 不会自动帮你切换回单行格式,因为它会把尾随逗号视为"保持多行"的明确指令。 ## 解决方案 解决方案是让 `zig fmt` 基于行长度自动决定使用单行还是多行格式。具体来说:如果一个表达式能放在一行内(不超过某个长度限制,比如 100 个字符),就使用单行格式;否则,使用多行格式。 这正是 `prettier` 的工作方式,也是大多数现代格式化工具所采用的方案。 这意味着尾随逗号将不再具有语义上的格式化含义。你可以写: ```zig const x = foo(1, 2, 3,); ``` `zig fmt` 会根据行长度自动决定使用哪种格式,而不是盲目地遵循尾随逗号的指示。 ## 争议点 这项改动之所以有争议,主要有以下几个原因: **1. 人们习惯了现有的行为** 很多 Zig 开发者已经习惯于用尾随逗号来控制格式化。改变这一行为会打破他们的工作流程。 **2. 基于行长度的格式化可能产生令人惊讶的结果** 当你重命名一个变量,导致某行超过了长度限制,整个表达式的格式可能会突然从单行变成多行。这种"蝴蝶效应"可能让人感到困惑。 **3. 需要确定合适的行长度限制** 100 个字符?80 个字符?这本身也是一个需要决策的问题,尽管它只是一次性的决策,而不是每次写代码都要面对的决策。 ## 结论 尽管存在争议,我认为基于行长度的自动格式化是正确的方向。它让 `zig fmt` 真正成为一个固执己见的格式化工具,消除了用户需要做出的格式化决策,并确保代码在重构后始终保持最优的格式。 这与 Zig 语言的整体设计哲学是一致的:减少不必要的复杂性,让工具为开发者做出明智的决策,从而让开发者能够专注于真正重要的事情。

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` 的"可操控"设计哲学让格式化工具成为你的合作伙伴,而不是独裁者: - **尾随逗号** → 强制多行展开 - **无尾随逗号** → 允许单行折叠 - **手动换行 + 统一列数** → 触发列式对齐 掌握这两个技巧,你就能在享受自动格式化便利的同时,保留对代码视觉呈现的精确控制。

Zig 0.16.0 发布说明:"Juicy Main"

Simon Willison's Blog

Zig 0.16.0 发布了一个名为 'Juicy Main' 的新特性,它为 main() 函数提供了依赖注入功能,方便地访问分配器、IO、环境变量和命令行参数。

Zig 构建速度正在提升

Mitchell Hashimoto

Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。