📘 第05课:错误处理

📋 本课目录错误集(Error Set) 错误联合类型 try与catch errdefer深入 错误跟踪 错误处理模式 性能对比 练习

错误集(Error Set)

Zig的错误集是编译期确定的枚举——没有异常,没有错误码,没有Result类型包装:

const std = @import("std");

// 定义错误集
const FileError = error{
    NotFound,
    PermissionDenied,
    IsDir,
    NotDir,
    FileTooBig,
    DiskFull,
};

// 错误集可以合并
const NetworkError = error{
    ConnectionRefused,
    Timeout,
    DnsFailed,
};

const AppError = FileError || NetworkError;

// anyerror: 所有可能错误的超集
fn riskyOperation() anyerror!void {
    // 可以返回任何错误
}

// 错误集推断
fn readConfig() !void {
    // 编译器自动推断错误集
    const file = try std.fs.cwd().openFile("config.toml", .{});
    defer file.close();
}

错误联合类型

错误联合类型E!T表示"要么是T,要么是E中的某个错误":

// E!T = 错误联合类型
const result: FileError!usize = 42;   // 成功
const err: FileError!usize = error.NotFound; // 失败

// 获取大小
fn getFileSize(path: []const u8) !u64 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();
    const stat = try file.stat();
    return stat.size;
}

// 错误联合类型与可选类型组合
fn findUser(id: u32) !?User {
    // 可能出错、可能找不到、可能找到
    const row = try db.query("SELECT * FROM users WHERE id=?", .{id});
    if (!row.next()) return null;
    return row.toUser();
}

try与catch

const std = @import("std");

pub fn main() !void {
    // try: 成功返回值,失败传播错误
    const file = try std.fs.cwd().openFile("data.txt", .{});
    // 等价于:
    // const file = std.fs.cwd().openFile("data.txt", .{}) catch |err| return err;

    // catch: 处理错误
    const size = getFileSize("data.txt") catch |err| switch (err) {
        error.FileNotFound => 0,
        error.PermissionDenied => 0,
        else => return err,  // 重新传播
    };

    // catch提供默认值
    const config = readConfig() catch defaultConfig;

    // catch + 副作用
    const conn = connect() catch |err| {
        std.debug.print("连接失败: {}\n", .{err});
        return err;
    };

    _ = file;
    _ = size;
    _ = config;
    _ = conn;
}

// 用catch实现重试
fn connectWithRetry(addr: []const u8, max_retries: u32) !Connection {
    var retries: u32 = 0;
    while (retries < max_retries) : (retries += 1) {
        return connect(addr) catch |err| switch (err) {
            error.ConnectionRefused => {
                std.debug.print("重试 {}/{}...\n", .{ retries + 1, max_retries });
                continue;
            },
            else => return err,
        };
    }
    return error.MaxRetriesExceeded;
}

errdefer深入

// errdefer: 只在错误路径执行的清理代码
fn createUser(allocator: std.mem.Allocator, name: []const u8) !*User {
    // 分配用户结构体
    const user = try allocator.create(User);
    errdefer allocator.destroy(user);  // 只在出错时销毁

    // 分配名字的副本
    const name_copy = try allocator.dupe(u8, name);
    errdefer allocator.free(name_copy);  // 只在出错时释放

    user.* = .{
        .name = name_copy,
        .created = std.time.timestamp(),
    };

    // 成功!user和name_copy由调用者管理
    return user;
}

// errdefer + 错误值捕获
fn initSystem() !void {
    errdefer |err| {
        std.debug.print("初始化失败: {}\n", .{err});
        cleanupPartial();
    }

    try initNetwork();
    try initDatabase();
    try initCache();
}

错误跟踪

Zig提供错误跟踪(Error Trace)来帮助调试错误传播链:

// 编译时启用错误跟踪
// zig build -Derror-tracing=true

// 或者运行时
pub fn main() !void {
    deepCall() catch |err| {
        std.debug.print("错误: {}\n", .{err});

        // 打印完整错误跟踪
        if (@errorReturnTrace()) |trace| {
            std.debug.dumpStackTrace(trace.*);
        }
    };
}

// 自定义错误消息
fn deepCall() !void {
    try level1();
}
fn level1() !void {
    try level2();
}
fn level2() !void {
    return error.DeepFailure;
}

错误处理模式

1. 分层错误处理

// 底层:具体错误集
const DbError = error{ ConnectionLost, QueryFailed, Timeout };
const CacheError = error{ Miss, Expired, Corrupted };

// 高层:合并错误集
fn getUser(id: u32) (DbError || CacheError)!User {
    // 先查缓存
    const cached = cacheGet(id) catch |err| switch (err) {
        error.Miss, error.Expired => {
            // 缓存未命中,查数据库
            return dbGet(id);
        },
        error.Corrupted => {
            // 缓存损坏,清除后查数据库
            cacheInvalidate(id);
            return dbGet(id);
        },
    };
    return cached;
}

2. 不可恢复 vs 可恢复

// 不可恢复:用std.debug.panic
fn criticalInit() void {
    const config = loadConfig() catch |err| {
        std.debug.panic("无法加载配置: {}", .{err});
    };
}

// 可恢复:传播错误
fn optionalInit() !void {
    const config = loadConfig() catch |err| switch (err) {
        error.FileNotFound => defaultConfig(),
        else => return err,
    };
}

性能对比

📊 错误处理开销(1M次调用)

方式时间(ms)说明
Zig try (成功路径)1.2几乎零开销
Zig catch (错误路径)3.5分支预测惩罚
C errno (成功路径)1.3线程局部存储访问
C++ try/catch (成功路径)0.8零成本异常
C++ try/catch (异常路径)850栈展开开销巨大
Rust Result (成功)1.2与Zig类似
Go error (成功)2.5if err != nil 开销
💡 Zig的错误处理在成功路径上几乎零开销——错误值作为返回值的一部分传递,不需要栈展开。

练习

📝 课后练习

  1. 自定义错误集:为文件解析器定义ParseError,包含5种以上错误
  2. errdefer链:写一个函数分配3个资源,用errdefer确保错误时全部释放
  3. 错误转换:将底层IO错误转换为应用级错误,保留原始错误信息
  4. 重试机制:实现通用的retry(comptime max: u32, func)
  5. 错误跟踪:写一个3层函数调用,每层用try传播,观察错误跟踪