Hey!
Billgo

EN

Zig 语言的错误处理机制

2025 年 8 月 20 日

我觉得 Zig 语言的错误处理机制堪称精妙。它独辟蹊径,巧妙地融合了编译时与运行时的优势,既保证了程序性能,又兼顾了代码的可读性和可维护性。

本文将带你粗略了解在 Zig 语言中是怎样处理错误的,一窥其设计巧思。

1)错误集

在 Zig 语言中,错误被视为而不是异常。我们可以用 error sets 来定义一个错误类型,这个类型包含预先指定的错误标签。

const FileError = error{
    NotFound,
    PermissionDenied,
    InvalidPath,
};

这段代码定义了一个名为 FileError 的错误集。FileError 类型的值只能是 NotFoundPermissionDeniedInvalidPath 中的一个。这类似于枚举,但专门用于表示错误类型。

2)错误联合体

Zig 使用错误联合体来将错误与常规值组合起来,语法是 !Terror{...}!T

  • !T:表示函数要么返回类型 T 的值,要么返回一个错误。
  • error{...}!T:更完整的写法,error{...} 指明了可能出现的错误集合。
fn readNumber(str: []const u8) !u32 {
    if (str.len == 0) return error.EmptyString;
    return std.fmt.parseInt(u32, str, 10);
}

pub fn main() !void {
    const num = try readNumber("123");
    std.debug.print("The number is: {}\n", .{num});

    // 这里会返回错误
    const bad_num = readNumber("abc") catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return err;
    };
}

在上面的例子中,readNumber 函数的返回值类型是 !u32,意味着这个函数要么返回一个 u32 类型的数字,要么返回一个错误。catch 关键字用于捕获错误,并允许我们自定义错误处理逻辑。

注意:这里的 ! 不是布尔“取反”,而是类型构造符,用来把“错误集合 + 载荷类型”合成为错误联合类型。

3)错误传播与捕获

Zig 提供了简洁优雅的错误处理方式:trycatch

  • try:相当于 catch |err| return err 的简写。如果 try 后面的表达式返回错误,try 会立即把错误向上层传播。
  • catch:用于捕获错误,并提供自定义错误处理逻辑。
fn processFile() !void {
    // try 会展开为 catch |err| return err
    const file = try std.fs.cwd().openFile("data.txt", .{});
    defer file.close();

    // 处理错误的不同方式
    const data = readData() catch |err| switch(err) {
        error.OutOfMemory => return error.CannotProcess,
        error.InvalidData => return error.BadFormat,
        else => return err,
    };

    _ = data;
}

try 适合把当前函数无法处理的错误继续交给调用方;catch 则适合在当前上下文中恢复、转换或记录错误。

4)错误是类型系统的一部分

Zig 的错误不是运行时才突然出现的异常,而是类型系统的一部分。函数签名会明确告诉调用方:这个函数可能失败。

这带来几个好处:

  • 调用方无法忽略错误路径。
  • 编译器可以帮助你检查错误传播是否合理。
  • 错误处理逻辑不会隐藏在隐式异常机制中。

这种设计让代码读起来更直接:你看到返回类型,就知道函数是否可能失败。

5)错误集合可以合并

在真实项目中,一个函数可能调用多个可能失败的函数。Zig 可以根据上下文推导和合并错误集合,让代码既保持类型安全,又不会显得冗长。

const ParseError = error{InvalidNumber};
const FileError = error{NotFound, PermissionDenied};

fn loadNumber(path: []const u8) (ParseError || FileError)!u32 {
    const file = openFile(path) catch |err| return err;
    return parseNumber(file) catch |err| return err;
}

这里的 (ParseError || FileError)!u32 表示函数可能返回两个错误集合中的任意错误,也可能返回一个 u32

总结

Zig 的错误处理不是异常,也不是简单的返回码,而是一套与类型系统深度结合的机制。

它的核心思路是:

  • 错误是值。
  • 失败路径写在类型里。
  • try 用于传播错误。
  • catch 用于处理错误。
  • 错误集合让错误边界更清晰。

这种机制让 Zig 的错误处理既显式又轻量。它要求开发者正视失败路径,但又尽量减少样板代码。这也是 Zig 在系统编程中非常吸引人的地方之一。