乐于分享
好东西不私藏

Zig 错误联合类型源码:比 Result 更优雅十倍 (第18篇)

Zig 错误联合类型源码:比 Result 更优雅十倍 (第18篇)

大家好,我是 Zachel,今天继续Zig源码学习系列。

Rust程序员最骄傲的事情之一,就是它的Result<T, E>类型——“错误处理终于安全了!”Go程序员则说:“multiple return values + error as last param,简单粗暴就完事了。”而Zig来了,直接甩出一句:

“你们这都叫什么错误处理?来,看看我的!T。”

今天我们不聊怎么用(基础教程太多了),我们直接潜入Zig编译器源码,看看这个错误联合类型(Error Union)到底是怎么在语言层面被实现的,为什么它能做到“比Result优雅十倍”。

1. 表面语法:一个叹号,两种命运

先复习一下最基础的写法:

const std = @import("std");pub fn openFile(path: []const u8) !std.fs.File {    // 可能返回错误,也可能返回文件    return std.fs.cwd().openFile(path, .{});}pub fn main() !void {    const file = try openFile("hello.txt");   // 自动传播错误    defer file.close();    // 或者显式处理    openFile("不存在的文件") catch |err| {        std.debug.print("惨了:{s}\n", .{@errorName(err)});    };}

注意这行:

!std.fs.File

这个!就是错误联合类型的核心标记。它等价于:

  • 要么是某种错误(来自某个error集合)
  • 要么是std.fs.File

Rust里你要写Result<File, std::io::Error>,模板参数一堆。Zig直接用一个叹号搞定。

但这叹号真的只是语法糖吗?并不是。

2. 源码真相:错误根本不是“值”

我们直接看Zig编译器自己怎么理解!T(基于master分支2025-2026时期的结构,主要看src/下的语义分析与类型系统部分)。

在Zig编译器里,类型系统把error_set ! payload_type表示成一种特殊的Type

  • ErrorUnion 类型节点(src/type.zig / src/AstGen.zig 等处)
  • 它内部有两个关键字段:
// 简化后的伪码(实际实现在 src/type.zig 和 src/Sema.zig)pub const ErrorUnion = struct {    error_set: *Type,          // 错误集合(可能是anyerror,也可能是你定义的MyErrors)    payload: *Type,            // 正常返回值类型    // ... 还有一些表示是否是inferred error set的标志};

最关键的一点来了:

错误(error value)在Zig里根本不是一个普通的“值”!

它不占用payload的内存空间。

Zig采用了一种非常激进的表示方式:

  • 当函数返回类型是E!T
  • 正常路径:返回的是T,寄存器 / ABI 完全按照T来传递
  • 错误路径:返回的是错误码,通常放在一个独立的错误返回槽(很多架构用第二个返回值寄存器,或者用栈上传递)

编译器在生成LLVM IR时,会把:

return error.OutOfMemory;

直接转成:

ret iX <错误码对应的整数>

return my_value;

则是

ret %T %my_value   ; 完全不带任何额外tag

这和Rust的Result完全不一样。

Rust的Result在内存里真的是一个tagged union(tag + payload),正常值和Err都要占同样的栈/寄存器空间(除非优化成niche)。

Zig的错误路径几乎零成本——它甚至不占用正常返回值的空间!

3. 为什么说比Result优雅十倍?

维度
Rust Result<T, E>
Zig E!T
点评
语法长度
Result<T, MyLongErrorName>
!T
短到离谱
内存布局
tagged union(tag + data)
分离通道:错误不占payload空间
Zig零成本错误路径
错误集合可组合
需要显式enum或thiserror宏
error{A}
错误推导
必须写全E
可以写anyerror!T 或干脆 !T(推导)
写得越少越安全
try/catch 表达力
? + match / unwrap_or 等
try / catch / errdefer / if … else
Zig的errdefer堪称神器
ABI友好度
C++风格,很难和C互操作
错误路径几乎不影响C ABI
Zig错误处理也能轻松做C库

尤其是最后一点:Zig的错误处理模型是唯一一个既安全又几乎不影响C ABI兼容性的模型

你写一个!i32的函数,正常返回i32时,汇编和i32函数一模一样;出错时才多一个错误码通道。这让Zig特别适合做底层库、嵌入式、操作系统内核。

4. 一个小而美的例子:errdefer

fn processFile(allocator: std.mem.Allocator) ![]u8 {    var file = try std.fs.cwd().openFile("data.txt", .{});    defer file.close();           // 正常退出 / 异常退出都会执行    var buffer = try allocator.alloc(u8, 1024*1024);    errdefer allocator.free(buffer);   // ← 只在出错路径执行!    // 假如这里出错了,buffer自动释放,file也close    try file.reader().readNoEof(buffer);    return buffer;}

Rust要实现类似效果,需要Drop + ? + 手动处理,或者用scopeguard之类的crate。

Zig一句errdefer就解决了——这也是因为错误是独立的控制流通道,编译器能精确知道“哪些代码只在错误路径执行”。

5. 总结:叹号的代价与魔法

Zig的!T并不是一个普通的union类型。

它是一种语言级别的、编译器深度优化的错误通道

  • 语法极简:一个叹号
  • 零成本错误路径
  • 自动错误集合合并
  • errdefer / try / catch 强大的控制流表达
  • 保留了接近C的ABI和性能

当你写下fn foo() !i32,你其实是在和编译器说:

“帮我开一条错误高速公路,但别让它干扰正常值的赛道。”

这就是Zig错误联合类型的黑魔法。

喜欢的话点个在看,我们下篇见~

(第18篇完)