Diagnostics Factory
摘要
该文章介绍了在Zig中使用诊断工厂模式来管理错误报告,避免预先定义错误类型,而是提供一组构造函数来生成错误信息,并展示了在TigerBeetle项目中的实际应用。
<header>
<h1>诊断工厂</h1>
<time class="meta" datetime="2026-02-16">2026年2月16日</time>
</header>
<p>在
<span class="display"><a href="https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html"><em>用错误码控制流程</em></a></span> 一文中,我解释了Zig的强类型错误码解决了错误管理的“处理”部分,而将“报告”部分留给了用户。今天,我想描述我个人处理报告问题的默认方式,即向用户展示有用的错误信息。</p>
<p>这种方法用反面描述最为贴切:<em>避免</em>思考错误载荷和错误类型应该是什么。取而代之的是,提供一组用于构造错误的函数。</p>
<p>举一个具体的例子,在TigerBeetle的
<a href="https://github.com/tigerbeetle/tigerbeetle/blob/0.16.73/src/tidy.zig#L54-L188"><code>tidy.zig</code></a>
(一个项目特定的代码检查脚本,是另一种有用的元模式)中,我们这样定义错误:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">const</span> Errors = <span class="hl-keyword">struct</span> {</span>
<span class="line"> <span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_long_line</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> file: SourceFile,</span>
<span class="line"> line_index: <span class="hl-type">usize</span>,</span>
<span class="line"> ) <span class="hl-type">void</span> { ... }</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_banned</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> file: SourceFile,</span>
<span class="line"> offset: <span class="hl-type">usize</span>,</span>
<span class="line"> banned_item: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> replacement: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> ) <span class="hl-type">void</span> { ... }</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_dead_declaration</span>(...) <span class="hl-type">void</span> { ... }</span>
<span class="line"></span>
<span class="line"> ...</span>
<span class="line">};</span></code></pre>
</figure>
<p>调用处看起来像这样:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">fn</span><span class="hl-function"> tidy_file</span>(file: SourceFile, errors: <span class="hl-operator">*</span>Errors) <span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-comment">// ...</span></span>
<span class="line"> <span class="hl-keyword">var</span> line_index: <span class="hl-type">usize</span> = <span class="hl-numbers">0</span>;</span>
<span class="line"> <span class="hl-keyword">while</span> (lines.next()) <span class="hl-operator">|</span>line<span class="hl-operator">|</span> : (line_index <span class="hl-operator">+=</span> <span class="hl-numbers">1</span>) {</span>
<span class="line"> <span class="hl-keyword">const</span> line_length = line_length(line);</span>
<span class="line"> <span class="hl-keyword">if</span> (line_length > <span class="hl-numbers">100</span> <span class="hl-keyword">and</span> <span class="hl-operator">!</span>contains_url(line)) {</span>
<span class="line"> errors.add_long_line(file, line_index);</span>
<span class="line"> }</span>
<span class="line"> }</span>
<span class="line">}</span></code></pre>
</figure>
<p>在这个例子中,我收集多个错误,因此不会立即返回。快速失败的情况会是这样:</p>
<figure class="code-block">
<pre><code><span class="line">errors.add_long_line(file, line_index);</span>
<span class="line"><span class="hl-keyword">return</span> <span class="hl-keyword">error</span>.Tidy;</span></code></pre>
</figure>
<p>注意,错误码故意独立于所产生的具体错误。</p>
<hr>
<p>该方案的一些有趣特性:</p>
<ul>
<li>
错误表示是一组构造函数,调用代码并不关心内部<em>实际</em>发生了什么。这就是为什么错误工厂是我的<em>默认</em>方案——我不需要事先想好如何处理错误,以后也可以改变主意。
</li>
<li>
有一个自然的地方可以将错误产生处的可用信息转换为用户可用的形式。在上面的<code>add_banned</code>中,调用者传入文件中的绝对偏移量,然后在内部解析为行号和列(提示:内部索引使用基于0的<code>line_index</code>,用户可见的使用基于1的<code>line_number</code>)。这与传统的求和类型错误方法形成对比,后者在直接构造变体和调用辅助函数之间存在明显的语法不连续性。
</li>
<li>
这种语法一致性反过来可以轻松地通过grep查找所有错误位置:
<span class="display"><code>rg 'errors.add_'</code>。</span>
</li>
<li>
同样,有一个中心位置列举了所有可能的错误(这既可能是优点也可能是缺点)。
</li>
</ul>
<p>一个不太明显的特性是这种结构支持多态性。实际上,在<code>tidy.zig</code>代码中,有两种不同的错误表示。运行脚本时,错误直接输出到stderr。但测试时,错误会被收集到内存缓冲区中:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_banned</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> file: SourceFile,</span>
<span class="line"> offset: <span class="hl-type">usize</span>,</span>
<span class="line"> banned_item: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> replacement: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line">) <span class="hl-type">void</span> {</span>
<span class="line"> errors.emit(</span>
<span class="line"> <span class="hl-string">"{s}:{d}: error: {s} is banned, use {s}<span class="hl-string">\n</span>"</span>,</span>
<span class="line"> .{</span>
<span class="line"> file.path, file.line_number(offset),</span>
<span class="line"> banned_item, replacement,</span>
<span class="line"> },</span>
<span class="line"> );</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hl-keyword">fn</span><span class="hl-function"> emit</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> <span class="hl-keyword">comptime</span> fmt: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> args: <span class="hl-type">anytype</span>,</span>
<span class="line">) <span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-keyword">comptime</span> assert(fmt[fmt.len <span class="hl-operator">-</span> <span class="hl-numbers">1</span>] <span class="hl-operator">==</span> <span class="hl-string">'<span class="hl-string">\n</span>'</span>);</span>
<span class="line"> errors.count <span class="hl-operator">+=</span> <span class="hl-numbers">1</span>;</span>
<span class="line"> <span class="hl-keyword">if</span> (erro
查看缓存全文
缓存时间: 2026/05/16 03:34
# 诊断工厂
来源:https://matklad.github.io/2026/02/16/diagnostics-factory.html
2026年2月16日
在*用于控制流的错误码*(https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html)中,我解释了 Zig 的强类型错误码解决了错误管理的“处理”部分,而将“报告”留给用户。今天,我想描述我个人在处理报告问题时的默认方法,即向用户展示有用的错误信息。这种方法最好用反面来描述:*避免*思考错误载荷以及错误应该是什么类型。相反,提供一组用于构造错误的函数。举个具体的例子,在 TigerBeetle 的 `tidy.zig`(https://github.com/tigerbeetle/tigerbeetle/blob/0.16.73/src/tidy.zig#L54-L188)(一个项目特定的 lint 脚本,另一个有用的元模式)中,我们这样定义错误:
```zig
const Errors = struct {
pub fn add_long_line(
errors: *Errors,
file: SourceFile,
line_index: usize,
) void {
...
}
pub fn add_banned(
errors: *Errors,
file: SourceFile,
offset: usize,
banned_item: []const u8,
replacement: []const u8,
) void {
...
}
pub fn add_dead_declaration(...) void { ... }
...
};
```
调用处看起来像这样:
```zig
fn tidy_file(file: SourceFile, errors: *Errors) void {
// ...
var line_index: usize = 0;
while (lines.next()) |line| : (line_index += 1) {
const line_length = line_length(line);
if (line_length > 100 and !contains_url(line)) {
errors.add_long_line(file, line_index);
}
}
}
```
在这个例子中,我收集多个错误,所以不会立即返回。快速失败看起来会是这样:
```zig
errors.add_long_line(file, line_index);
return error.Tidy;
```
请注意,错误码故意与产生的具体错误无关。
---
该方法的一些有趣特性:
- 错误表示是一组构造函数,调用代码并不关心内部*实际*发生了什么。这正是为什么错误工厂是我的*默认*方案——我不需要预先想好如何处理错误,之后也可以改变主意。
- 在将错误产生处可用的信息转换为对用户有用的形式时,有一个自然的转换点。在上面的 `add_banned` 中,调用者传入文件中的绝对偏移量,然后在内部解析为行号和列号(提示:使用 `line_index` 表示从 0 开始的内部索引,使用 `line_number` 表示用户可见的从 1 开始的编号)。这与传统的将错误作为 sum-type 的方法形成对比,后者在直接构造变体和调用辅助函数之间存在明显的语法断裂。
- 这种语法一致性反过来使得可以轻松 grep 所有错误位置:`rg 'errors\.add_'`。
- 类似地,有一个中心位置枚举了所有可能的错误(这既可以是优点也可以是缺点)。
一个更微妙的特性是,这种结构支持多态。实际上,在 `tidy.zig` 代码中,存在两种不同的错误表示。运行脚本时,错误直接输出到 stderr。但测试时,错误会被收集到内存缓冲区中:
```zig
pub fn add_banned(
errors: *Errors,
file: SourceFile,
offset: usize,
banned_item: []const u8,
replacement: []const u8,
) void {
errors.emit(
"{s}:{d}: error: {s} is banned, use {s}\n",
.{ file.path, file.line_number(offset), banned_item, replacement },
);
}
fn emit(
errors: *Errors,
comptime fmt: []const u8,
args: anytype,
) void {
comptime assert(fmt[fmt.len - 1] == '\n');
errors.count += 1;
if (errors.captured) |*captured| {
captured.writer(errors.gpa).print(fmt, args) catch @panic("OOM");
} else {
std.debug.print(fmt, args);
}
}
```
这里并不存在一个巨大的所有错误的 `union(enum)`,因为对于当前用例来说并不需要。这种模式可以进一步扩展为完整的诊断框架,包含错误构建器、跨度、ANSI 颜色等,但这与这里的核心思想无关:即使在“小型编程”中,避免直接构造枚举,强制通过中间函数调用,可能也是一个好主意。
---
还有两个元观察:
*第一*,整个模式当然是一个类型和两个函数的乘积(访问者模式)的二元性体现。
```rust
fn foo() -> Result;
fn bar(ok: impl FnOnce(T), err: impl FnOnce(E));
```
```rust
enum Result { Ok(T), Err(E), }
trait Result { fn ok(self, T); fn err(self, E); }
```
*第二*,每个抽象都是一层薄薄的薄膜,分隔两大块代码。任何接口都有两面:一面是用户熟悉的,另一面是隐藏的,提供给实现者。通常,默认的语言机制会促使你对两者使用相同的构造,但这可能不是最优选择。用户和抽象的提供者很自然地在最优接口上存在分歧,并且独立演进。使用一个大的枚举来统一错误,会将错误产生和错误报告代码耦合在一起,因为它们必须在中间相遇。相比之下,工厂方案对生产者是最优的(他们只需直接传递手头已有的东西,无需额外处理数据),对消费者则是灵活的。
相似文章
@0xKingsKuan: 自行修 Bug,可观测性工具! 以前搞生产系统有多崩溃? 日志,trace、metrics 铺天盖地,关键错误被噪声淹没,出了问题只能手动翻日志、猜调用链,排查半天还是不知道根因,更别说自动修复了。 现在好了,superloglabs 直…
Superloglabs 开源了 Superlog,一个基于 OpenTelemetry 的 agentic 可观测性平台,能自动聚类事件、生成 Incident 并协助修复 Bug。
最小可行的Zig错误上下文
一篇博文,详细介绍了使用errdefer日志在Zig中添加错误上下文的最小模式,并将其与完整的诊断接收器(diagnostics sinks)和catch块进行比较,讨论了权衡取舍。
你必须修复你的断言
本文认为在生产环境中禁用断言是一种不好的实践,以Zig的断言机制为例,说明即使在生产版本中也应保持断言启用以捕获编程错误的好处。
代理失败聚类改变了我对调试的思考方式
一位开发者分享了在多个代理运行中可视化失败聚类如何改变了他们的调试方法,强调了建立反馈循环的必要性,使代理能够从过去的错误中学习,而不是将失败视为孤立的问题。文章提到了手动变通方法和一个名为BentoLabs的平台,该平台实现了闭环改进。
@thinkszyg: https://x.com/thinkszyg/status/2066837941477920993
一篇面向开发者(尤其是AI编码工具使用者)的实用指南,介绍如何安全高效地使用Claude Code、Codex等工具进行多Agent并行开发,重点包括任务拆解、文件隔离(worktree)、边界控制、顺序合并等最佳实践,避免文件冲突和混乱。