📘 第04课:错误处理(error/try/catch)

📋 本课目录错误集错误联合类型try与catcherrdefer错误跟踪自定义错误课后练习

错误集(Error Set)

Zig用错误集替代异常和错误码。错误是编译期枚举值,零开销传递:

const std = @import("std");

// 定义错误集
const OpenError = error{
    FileNotFound,
    PermissionDenied,
    DiskFull,
    IsDir,
};

const ReadError = error{
    EndOfStream,
    ConnectionReset,
};

// 错误集可以合并
const IoError = OpenError || ReadError;

pub fn main() !void {
    // 错误值
    const err: OpenError = error.FileNotFound;

    // 错误可以比较
    std.debug.print("是FileNotFound: {}\n", .{err == error.FileNotFound});

    // 获取错误名称
    std.debug.print("错误名: {s}\n", .{@errorName(err)});

    // 全局错误集——任意错误
    const any_err: anyerror = err;

    _ = IoError; _ = any_err;
}

错误联合类型(Error Union)

E!T表示"返回T或者错误E":

const std = @import("std");

// 显式错误集:!T 等价于 anyerror!T
fn parseU8(str: []const u8) !u8 {
    const val = try std.fmt.parseUnsigned(u8, str, 10);
    return val;
}

// 精确错误集
fn safeDivide(a: i32, b: i32) error{DivByZero, Overflow}!i32 {
    if (b == 0) return error.DivByZero;
    const result = @divTrunc(a, b);
    // 检查溢出
    if (a != 0 and result * b != a) return error.Overflow;
    return result;
}

pub fn main() !void {
    // 精确错误集让编译器推断可能的所有错误
    const result = safeDivide(10, 3);
    const val = result catch |err| switch (err) {
        error.DivByZero => 0,
        error.Overflow => std.math.maxInt(i32),
    };
    std.debug.print("safeDivide(10,3) = {}\n", .{val});

    // 类型推断
    const x: u8 = try parseU8("42");
    std.debug.print("parsed: {}\n", .{x});
}

性能分析

错误联合类型在内存中用额外位标记成功/错误,比C的返回码+errno更高效:返回值和错误状态在一次寄存器操作中传递,无需访问全局errno。Debug模式包含错误跟踪,ReleaseFast自动消除。

try与catch

const std = @import("std");

// try: 成功返回值,失败向上传播错误
fn readConfig() ![]const u8 {
    const file = try std.fs.cwd().openFile("config.txt", .{});
    defer file.close();

    var buf: [1024]u8 = undefined;
    const reader = file.reader();
    const len = try reader.readAll(&buf);
    return buf[0..len];
}

pub fn main() !void {
    // catch处理错误
    const config = readConfig() catch |err| switch (err) {
        error.FileNotFound => "default",
        error.PermissionDenied => "denied",
        else => return err,  // 未知错误继续传播
    };

    // catch提供默认值
    const val = parseU8("abc") catch 0;

    // catch返回错误
    const num = parseU8("99") catch unreachable;

    // if-try组合
    if (readConfig()) |data| {
        std.debug.print("配置: {s}\n", .{data});
    } else |err| {
        std.debug.print("读取失败: {}\n", .{err});
    }

    _ = config; _ = val; _ = num;
}

fn parseU8(str: []const u8) !u8 {
    return std.fmt.parseUnsigned(u8, str, 10);
}

errdefer深入

const std = @import("std");

// errdefer + 捕获错误值
fn complexInit(allocator: std.mem.Allocator) !void {
    const buf1 = try allocator.alloc(u8, 1024);
    errdefer allocator.free(buf1);

    const buf2 = try allocator.alloc(u8, 2048);
    errdefer allocator.free(buf2);

    const buf3 = try allocator.alloc(u8, 4096);
    errdefer allocator.free(buf3);

    // 如果buf3分配失败,buf1和buf2会被自动释放
    // 如果buf2分配失败,buf1会被自动释放

    // 成功路径
    allocator.free(buf3);
    allocator.free(buf2);
    allocator.free(buf1);
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    try complexInit(gpa.allocator());
    std.debug.print("初始化成功\n", .{});
}

错误跟踪(Error Trace)

Debug模式下Zig提供完整错误跟踪,比C的backtrace更有用:

// 编译时加 --debug-include-errors 或用 Debug 模式
// 运行时错误会显示完整调用链

fn deep1() !void { try deep2(); }
fn deep2() !void { try deep3(); }
fn deep3() !void { return error.DeepError; }

// 错误跟踪会显示:
// error: DeepError
// deep3 at main.zig:3
// deep2 at main.zig:2
// deep1 at main.zig:1

自定义错误处理模式

const std = @import("std");

// 重试模式
fn retryableOp(max_retries: u32) !void {
    var attempts: u32 = 0;
    while (attempts < max_retries) : (attempts += 1) {
        return mightFail() catch |err| switch (err) {
            error.Transient => {
                std.debug.print("重试 {}/{}\n", .{ attempts + 1, max_retries });
                continue;
            },
            else => return err,
        };
    }
    return error.MaxRetriesExceeded;
}

const TransientError = error{ Transient, Permanent };
fn mightFail() TransientError!void {
    if (std.crypto.random.int(u1) == 0) return error.Transient;
    return;
}

// 错误转换模式
fn wrapIoError(err: std.fs.File.OpenError) error{ConfigError} {
    _ = err;
    return error.ConfigError;
}

pub fn main) !void {
    try retryableOp(3);
    std.debug.print("操作成功\n", .{});
}

课后练习

📝 练习1:错误传播

编写一个读取配置文件的函数链,每步都可能失败,使用try传播

📝 练习2:错误恢复

实现一个HTTP请求重试函数,对临时错误重试3次

📝 练习3:errdefer链

用errdefer实现3步资源分配,任意步骤失败都正确释放已分配资源

📝 练习4:自定义错误集

为JSON解析器定义详细错误集:语法错误、类型错误、溢出等

📝 练习5:错误映射

实现一个函数将底层IO错误映射为业务层错误

🏆 成就解锁:错误处理专家