Chapter 57Error Handling Patterns Cookbook

附录C. 错误处理模式手册

概述

第4章介绍了Zig错误联合、tryerrdefer的机制;本附录将这些想法转化为快速参考手册,您可以在起草新API或重构现有API时参考。每个配方都加强了领域特定错误词汇和用户最终看到的诊断消息之间的联系。

Zig 0.15.2改进了整数转换和分配器失败周围的诊断,使得在调试和发布安全构建中更容易依赖精确的错误传播。v0.15.2

学习目标

  • 在标准Zig I/O失败之上分层领域特定错误集,而不失去精度。
  • 使用errdefer保护堆支持转换,以便每个退出路径都配对分配和释放。
  • 将内部错误联合转换为日志和用户界面的清晰、可操作的消息。

_Refs: _

分层错误词汇

当子系统引入自己的错误条件时,完善词汇而不是将所有东西投入到anyerror。下面的模式从解析失败和模拟I/O错误组合配置特定的联合,以便调用者永远不会失去对NotFoundInvalidPort的追踪。4catch |err| switch习惯用法保持映射诚实,并镜像std.fmt.parseInt如何呈现解析问题。fmt.zig

Zig
//! Demonstrates layering domain-specific error sets when loading configuration.
const std = @import("std");

pub const ParseError = error{
    MissingField,
    InvalidPort,
};

pub const SourceError = error{
    NotFound,
    PermissionDenied,
};

pub const LoadError = SourceError || ParseError;

const SimulatedSource = struct {
    payload: ?[]const u8 = null,
    failure: ?SourceError = null,

    fn fetch(self: SimulatedSource) SourceError![]const u8 {
        if (self.failure) |err| return err;
        return self.payload orelse SourceError.NotFound;
    }
};

fn parsePort(text: []const u8) ParseError!u16 {
    var iter = std.mem.splitScalar(u8, text, '=');
    const key = iter.next() orelse return ParseError.MissingField;
    const value = iter.next() orelse return ParseError.MissingField;
    if (!std.mem.eql(u8, key, "PORT")) return ParseError.MissingField;
    return std.fmt.parseInt(u16, value, 10) catch ParseError.InvalidPort;
}

pub fn loadPort(source: SimulatedSource) LoadError!u16 {
    const line = source.fetch() catch |err| switch (err) {
        SourceError.NotFound => return LoadError.NotFound,
        SourceError.PermissionDenied => return LoadError.PermissionDenied,
    };

    return parsePort(line) catch |err| switch (err) {
        ParseError.MissingField => return LoadError.MissingField,
        ParseError.InvalidPort => return LoadError.InvalidPort,
    };
}

test "successful load yields parsed port" {
    const source = SimulatedSource{ .payload = "PORT=8080" };
    try std.testing.expectEqual(@as(u16, 8080), try loadPort(source));
}

test "parse errors bubble through composed union" {
    const source = SimulatedSource{ .payload = "HOST=example" };
    try std.testing.expectError(LoadError.MissingField, loadPort(source));
}

test "source failures remain precise" {
    const source = SimulatedSource{ .failure = SourceError.PermissionDenied };
    try std.testing.expectError(LoadError.PermissionDenied, loadPort(source));
}
运行
Shell
$ zig test 01_layered_error_sets.zig
输出
Shell
All 3 tests passed.

将原始错误名称一直保留到您的API边界——调用者可以明确分支到LoadError.PermissionDenied,这比字符串匹配或哨兵值更稳健。36

用于平衡清理的errdefer

字符串组装和JSON整形经常分配临时缓冲区;当验证步骤失败时忘记释放它们会直接导致泄漏。通过将std.ArrayListUnmanagederrdefer配对,下一个配方保证成功和失败路径都正确清理,同时仍返回方便的所有切片。13 这里使用的每个分配辅助函数都在标准库中提供,因此相同的结构可以扩展到更复杂的构建器。array_list.zig

Zig
//! Shows how errdefer keeps allocations balanced when joining user snippets.
const std = @import("std");

pub const SnippetError = error{EmptyInput} || std.mem.Allocator.Error;

pub fn joinUpperSnippets(allocator: std.mem.Allocator, parts: []const []const u8) SnippetError![]u8 {
    if (parts.len == 0) return SnippetError.EmptyInput;

    var list = std.ArrayListUnmanaged(u8){};
    errdefer list.deinit(allocator);

    for (parts, 0..) |part, index| {
        if (index != 0) try list.append(allocator, ' ');
        for (part) |ch| try list.append(allocator, std.ascii.toUpper(ch));
    }

    return list.toOwnedSlice(allocator);
}

test "joinUpperSnippets capitalizes and joins input" {
    const allocator = std.testing.allocator;
    const result = try joinUpperSnippets(allocator, &[_][]const u8{ "zig", "cookbook" });
    defer allocator.free(result);

    try std.testing.expectEqualStrings("ZIG COOKBOOK", result);
}

test "joinUpperSnippets surfaces empty-input error" {
    const allocator = std.testing.allocator;
    try std.testing.expectError(SnippetError.EmptyInput, joinUpperSnippets(allocator, &[_][]const u8{}));
}
运行
Shell
$ zig test 02_errdefer_join_upper.zig
输出
Shell
All 2 tests passed.

因为标准测试分配器自动触发泄漏,锻炼成功和错误分支兼作未来编辑的回归测试工具。13

为人类翻译错误

即使是精心制作的错误集也需要以富有同理心的语言落地。最后一个模式演示了如何为程序调用者保持原始ApiError,同时为日志或UI副本生成人类可读的散文。36std.io.fixedBufferStream使输出对测试具有确定性,专用格式化器将消息与控制流隔离。log.zig

Zig
//! Bridges domain errors to user-facing log messages.
const std = @import("std");

pub const ApiError = error{
    NotFound,
    RateLimited,
    Backend,
};

fn describeApiError(err: ApiError, writer: anytype) !void {
    switch (err) {
        ApiError.NotFound => try writer.writeAll("resource not found; check identifier"),
        ApiError.RateLimited => try writer.writeAll("rate limit exceeded; retry later"),
        ApiError.Backend => try writer.writeAll("upstream dependency failed; escalate"),
    }
}

const Action = struct {
    outcomes: []const ?ApiError,
    index: usize = 0,

    fn invoke(self: *Action) ApiError!void {
        if (self.index >= self.outcomes.len) return;
        const outcome = self.outcomes[self.index];
        self.index += 1;
        if (outcome) |err| {
            return err;
        }
    }
};

pub fn runAndReport(action: *Action, writer: anytype) !void {
    action.invoke() catch |err| {
        try writer.writeAll("Request failed: ");
        try describeApiError(err, writer);
        return;
    };
    try writer.writeAll("Request succeeded");
}

test "runAndReport surfaces friendly error message" {
    var action = Action{ .outcomes = &[_]?ApiError{ApiError.NotFound} };
    var buffer: [128]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);

    try runAndReport(&action, stream.writer());
    const message = stream.getWritten();
    try std.testing.expectEqualStrings("Request failed: resource not found; check identifier", message);
}

test "runAndReport acknowledges success" {
    var action = Action{ .outcomes = &[_]?ApiError{null} };
    var buffer: [64]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);

    try runAndReport(&action, stream.writer());
    const message = stream.getWritten();
    try std.testing.expectEqualStrings("Request succeeded", message);
}
运行
Shell
$ zig test 03_error_reporting_bridge.zig
输出
Shell
All 2 tests passed.

保持桥接函数纯粹——它应该只依赖于错误有效负载和写入器——以便消费者可以交换日志后端或在测试期间在内存中捕获诊断。36

需要掌握的模式

  • 将较低级错误逐字冒泡到最后一个负责任的边界,然后在一次地方转换它们以保持不变式明显。4
  • errdefer视为握手:每个分配或文件打开都应在同一作用域内进行匹配的清理。fs.zig
  • 为每个公共错误联合提供专用格式化器,以便文档和用户消息永远不会偏离。36

注意事项和警告

  • 使用||合并错误集保留标签但不保留有效负载数据;如果您需要结构化有效负载,请改用标记联合。
  • 分配器支持的辅助函数应直接暴露std.mem.Allocator.Error——调用者期望像标准库容器一样try分配。
  • 这里的配方假设调试或发布安全构建;在发布快速中,您可能希望为否则会触发unreachable的分支添加额外的日志记录。37

练习

  • 扩展loadPort,使其返回包含主机和端口的结构化配置对象,然后枚举生成的复合错误集。4
  • 添加joinUpperSnippets的流式变体,该变体写入用户提供的写入器而不是分配,并比较其人体工程学。Io.zig
  • 通过注入格式化器回调来教runAndReport在记录日志之前编辑标识符——使用单元测试验证成功和失败路径都尊重钩子。36

替代方案和边界情况

  • 对于长时间运行的服务,考虑使用指数退避和抖动包装重试循环;第29章重新审视并发影响。29
  • 如果您的错误桥需要本地化,请将消息ID与错误标签一起存储,并让更高层格式化最终字符串。
  • 具有微小分配器的嵌入式目标可能更喜欢基于栈的缓冲区或固定的std.BoundedArray实例,而不是堆支持的数组,以避免OutOfMemory10

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.