Ziglings 笔记 29: 失败时的后悔药 (errdefer)

只有出错时才执行

在学习了 defer(总是执行)之后,Ziglings 的第 29 个练习向我们展示了它的条件变体:errdefer

在系统编程中,我们经常遇到这样的场景:我做了一半的工作(比如打开了一个数据库连接),准备做下一半(查询数据)。如果查询失败了,我需要回滚之前的操作(关闭连接);但如果查询成功了,我要把连接留给调用者使用。

errdefer 就是为此而生的。

挑战:条件性日志

我们需要完善 makeNumber 函数,使得它:

  1. 总是打印 “Getting number…”。
  2. 如果成功拿到数字,打印 “got X.”。
  3. 只有在失败时,打印 “failed!”。

解决方案

使用 errdefer 来挂载错误处理逻辑:

const std = @import("std");

var counter: u32 = 0;
const MyErr = error{ GetFail, IncFail };

pub fn main() void {
    // 第一次尝试:成功
    const a: u32 = makeNumber() catch return;
    
    // 第二次尝试:失败(main 函数会在这里通过 catch return 退出)
    const b: u32 = makeNumber() catch return;

    std.debug.print("Numbers: {}, {}\n", .{ a, b });
}

fn makeNumber() MyErr!u32 {
    std.debug.print("Getting number...", .{});

    // 核心代码:errdefer
    // 这行代码只有在当前函数返回错误时才会执行
    errdefer std.debug.print("failed!\n", .{});

    var num = try getNumber(); 
    
    // 这里可能会出错!如果出错,上面的 errdefer 就会被触发
    num = try increaseNumber(num); 

    // 如果运行到这里,说明没有错误,errdefer 将会被忽略
    std.debug.print("got {}. ", .{num});

    return num;
}

// ... 辅助函数省略 ...

核心知识点总结

1. errdefer vs defer

  • defer: 这里的代码是清理工,不管房间有人没人,每天结束都得打扫。
  • errdefer: 这里的代码是保险员,只有出事故了才出现。

2. 完美的“事务”处理

errdefer 使得编写事务性代码变得非常整洁。

const object = createObject();
errdefer destroyObject(object); // 如果后续步骤失败,销毁它

try doSomethingWith(object);
try doAnotherThing(object);

return object; // 如果一切顺利,对象被返回,不会被销毁

这种模式避免了复杂的 if (error) { cleanup(); return error; } 嵌套。

3. 程序输出分析

运行这个程序,你会看到: Getting number...got 5. Getting number...failed! 注意最后一行没有打印 Numbers: ...,因为 main 函数在第二次调用失败时直接 return 退出了。这展示了错误是如何沿着调用栈一路传播并触发沿途的 errdefer 的。


后续预告:我们已经掌握了大部分控制流。接下来,我们要面对 Zig 中最强大的结构之一,它能替代 if-else 链,甚至能做更多事情——Switch 语句。