name: zig-best-practices description: 提供Zig语言的类型优先开发模式的最佳实践,包括标记联合、显式错误集、编译时验证和内存管理。阅读或编写Zig文件时必须使用。
Zig最佳实践
类型优先开发
类型定义了实现之前的契约。遵循此工作流程:
- 定义数据结构 - 首先定义结构体、联合和错误集
- 定义函数签名 - 参数、返回类型和错误联合
- 实现以满足类型 - 让编译器指导完整性
- 在编译时验证 - 在编译期间捕获无效配置
使非法状态无法表示
使用Zig的类型系统在编译时防止无效状态。
用于互斥状态的标记联合:
// 好的:只允许有效的组合
const RequestState = union(enum) {
idle,
loading,
success: []const u8,
failure: anyerror,
};
fn handleState(state: RequestState) void {
switch (state) {
.idle => {},
.loading => showSpinner(),
.success => |data| render(data),
.failure => |err| showError(err),
}
}
// 坏的:允许无效的组合
const RequestState = struct {
loading: bool,
data: ?[]const u8,
err: ?anyerror,
};
显式错误集用于失败模式:
// 好的:精确记录可能失败的地方
const ParseError = error{
InvalidSyntax,
UnexpectedToken,
EndOfInput,
};
fn parse(input: []const u8) ParseError!Ast {
// 实现
}
// 坏的:anyerror隐藏失败模式
fn parse(input: []const u8) anyerror!Ast {
// 实现
}
为领域概念使用不同的类型:
// 防止混淆不同类型的ID
const UserId = enum(u64) { _ };
const OrderId = enum(u64) { _ };
fn getUser(id: UserId) !User {
// 编译器防止在这里传递OrderId
}
fn createUserId(raw: u64) UserId {
return @enumFromInt(raw);
}
用于不变性的编译时验证:
fn Buffer(comptime size: usize) type {
if (size == 0) {
@compileError("buffer size must be greater than 0");
}
if (size > 1024 * 1024) {
@compileError("buffer size exceeds 1MB limit");
}
return struct {
data: [size]u8 = undefined,
len: usize = 0,
};
}
用于可扩展性的非穷尽枚举:
// 外部枚举可能会增加变体
const Status = enum(u8) {
active = 1,
inactive = 2,
pending = 3,
_,
};
fn processStatus(status: Status) !void {
switch (status) {
.active => {},
.inactive => {},
.pending => {},
_ => return error.UnknownStatus,
}
}
模块结构
在Zig中,较大的内聚文件是符合习惯的。将相关代码放在一起:测试与实现一起,编译时泛型在文件范围内,通过pub控制公共/私有。只有在文件处理真正独立的关注点时才分割。标准库通过像std/mem.zig这样的文件展示了这种模式,其中包含2000多行内聚的内存操作。
指令
- 使用错误联合(
!T)返回错误上下文;每个函数返回一个值或一个错误。显式错误集记录失败模式。 - 使用
errdefer进行错误路径上的清理;使用defer进行无条件清理。这可以防止资源泄漏,而无需try-finally样板代码。 - 在
switch语句中处理所有分支;包括一个返回错误或使用unreachable的else子句,用于真正不可能的情况。 - 显式传递需要动态内存的函数的分配器;在测试中优先使用
std.testing.allocator进行泄漏检测。 - 优先使用
const而不是var;对于边界安全,优先使用切片而不是原始指针。不可变性表明意图并启用优化。 - 避免
anytype;优先使用显式的comptime T: type参数。显式类型记录意图并产生更清晰的错误消息。 - 使用
std.log.scoped进行命名空间日志记录;为整个文件定义一个模块级log常量,以实现一致的范围。 - 为新逻辑添加或更新测试;使用
std.testing.allocator自动捕获内存泄漏。
示例
未实现逻辑的显式失败:
fn buildWidget(widget_type: []const u8) !Widget {
return error.NotImplemented;
}
使用try传播错误:
fn readConfig(path: []const u8) !Config {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const contents = try file.readToEndAlloc(allocator, max_size);
return parseConfig(contents);
}
使用errdefer进行资源清理:
fn createResource(allocator: std.mem.Allocator) !*Resource {
const resource = try allocator.create(Resource);
errdefer allocator.destroy(resource);
resource.* = try initializeResource();
return resource;
}
穷尽switch与显式默认:
fn processStatus(status: Status) ![]const u8 {
return switch (status) {
.active => "processing",
.inactive => "skipped",
_ => error.UnhandledStatus,
};
}
使用内存泄漏检测进行测试:
const std = @import("std");
test "widget creation" {
const allocator = std.testing.allocator;
var list: std.ArrayListUnmanaged(u32) = .empty;
defer list.deinit(allocator);
try list.append(allocator, 42);
try std.testing.expectEqual(1, list.items.len);
}
内存管理
- 显式传递分配器;永远不要使用全局状态进行分配。函数在参数中声明其分配需求。
- 使用
defer立即在获取资源后。将清理逻辑放在获取旁边以清晰明了。 - 优先使用arena分配器进行临时分配;它们在arena销毁时一次性释放所有内容。
- 在测试中使用
std.testing.allocator;它会报告泄漏,并显示显示分配来源的堆栈跟踪。
示例
显式参数分配器:
fn processData(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
const result = try allocator.alloc(u8, input.len * 2);
errdefer allocator.free(result);
// 处理输入到结果
return result;
}
批量操作的arena分配器:
fn processBatch(items: []const Item) !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
for (items) |item| {
const processed = try processItem(allocator, item);
try outputResult(processed);
}
// 当arena deinits时释放所有分配
}
日志记录
- 使用
std.log.scoped创建命名空间日志记录器;每个模块应定义自己的命名空间日志记录器以进行过滤。 - 在文件顶部定义一个模块级
const log;在整个模块中一致使用它。 - 使用适当的日志级别:
err用于失败,warn用于可疑条件,info用于状态变化,debug用于跟踪。
示例
模块的命名空间日志记录器:
const std = @import("std");
const log = std.log.scoped(.widgets);
pub fn createWidget(name: []const u8) !Widget {
log.debug("creating widget: {s}", .{name});
const widget = try allocateWidget(name);
log.debug("created widget id={d}", .{widget.id});
return widget;
}
pub fn deleteWidget(id: u32) void {
log.info("deleting widget id={d}", .{id});
// 清理
}
代码库中的多个范围:
// 在 src/db.zig
const log = std.log.scoped(.db);
// 在 src/http.zig
const log = std.log.scoped(.http);
// 在 src/auth.zig
const log = std.log.scoped(.auth);
编译时模式
- 使用
comptime参数进行泛型函数;类型信息在编译时可用,无需运行时成本。 - 优先编译时验证而不是运行时检查。在生产中而不是在编译期间捕获错误。
- 使用
@compileError用于无效配置,应该使构建失败。
示例
带有编译时类型的泛型函数:
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
编译时验证:
fn createBuffer(comptime size: usize) [size]u8 {
if (size == 0) {
@compileError("buffer size must be greater than 0");
}
return [_]u8{0} ** size;
}
避免anytype
- 优先
comptime T: type而不是anytype;显式类型参数记录预期约束并产生更清晰的错误。 - 仅当函数真正接受任何类型(如
std.debug.print)或用于回调/闭包时,使用anytype。 - 使用
anytype时,添加一个文档注释,描述预期的接口或约束。
示例
优先显式编译时类型(好):
fn sum(comptime T: type, items: []const T) T {
var total: T = 0;
for (items) |item| {
total += item;
}
return total;
}
当类型已知时避免anytype(坏):
// 不清楚哪些类型是有效的;错误消息将会令人困惑
fn sum(items: anytype) @TypeOf(items[0]) {
// ...
}
可接受的anytype用于回调:
/// 对每个项目调用`callback`。回调必须接受(T)并返回void。
fn forEach(comptime T: type, items: []const T, callback: anytype) void {
for (items) |item| {
callback(item);
}
}
当anytype必要时使用@TypeOf:
fn debugPrint(value: anytype) void {
const T = @TypeOf(value);
if (@typeInfo(T) == .Pointer) {
std.debug.print("ptr: {*}
", .{value});
} else {
std.debug.print("val: {}
", .{value});
}
}
错误处理模式
- 为函数定义特定的错误集;尽可能避免使用
anyerror。特定错误记录失败模式。 - 使用带有块的
catch进行错误恢复或日志记录;仅当错误确实不可能时使用catch unreachable。 - 使用
||合并错误集,当组合可能以不同方式失败的操作时。
示例
特定错误集:
const ConfigError = error{
FileNotFound,
ParseError,
InvalidFormat,
};
fn loadConfig(path: []const u8) ConfigError!Config {
// 实现
}
带有catch块的错误处理:
const value = operation() catch |err| {
std.log.err("operation failed: {}", .{err});
return error.OperationFailed;
};
配置
- 在启动时从环境变量加载配置;在使用之前验证所需的值。缺少配置应导致干净退出,并带有描述性消息。
- 定义Config结构作为单一真相源;避免在代码中散布
std.posix.getenv。 - 为开发使用合理的默认值;要求生产机密显式值。
示例
类型化配置结构:
const std = @import("std");
pub const Config = struct {
port: u16,
database_url: []const u8,
api_key: []const u8,
env: []const u8,
};
pub fn loadConfig() !Config {
const db_url = std.posix.getenv("DATABASE_URL") orelse
return error.MissingDatabaseUrl;
const api_key = std.posix.getenv("API_KEY") orelse
return error.MissingApiKey;
const port_str = std.posix.getenv("PORT") orelse "3000";
const port = std.fmt.parseInt(u16, port_str, 10) catch
return error.InvalidPort;
return .{
.port = port,
.database_url = db_url,
.api_key = api_key,
.env = std.posix.getenv("ENV") orelse "development",
};
}
可选性
- 使用
orelse为可选性提供默认值;仅当null是程序错误时使用.?。 - 优先
if (optional) |value|模式进行安全解包,并访问值。
示例
安全可选处理:
fn findWidget(id: u32) ?*Widget {
// 查找实现
}
fn processWidget(id: u32) !void {
const widget = findWidget(id) orelse return error.WidgetNotFound;
try widget.process();
}
可选性与if解包:
if (maybeValue) |value| {
try processValue(value);
} else {
std.log.warn("no value present", .{});
}
高级主题
参考这些指南以获取专业模式:
- 构建自定义容器(队列、栈、树):见GENERICS.md
- 与C库接口(raylib、SDL、curl、系统API):见C-INTEROP.md
- 调试内存泄漏(GPA、堆栈跟踪):见DEBUGGING.md
工具
zigdoc - 文档查找
用于浏览Zig标准库和项目依赖项文档的CLI工具。
安装:
git clone https://github.com/rockorager/zigdoc
cd zigdoc
zig build install -Doptimize=ReleaseFast --prefix $HOME/.local
使用:
zigdoc std.ArrayList # 标准库符号
zigdoc std.mem.Allocator # 嵌套符号
zigdoc vaxis.Window # 项目依赖项(如果在build.zig中)
zigdoc @init # 创建AGENTS.md与API模式
ziglint - 静态分析
用于Zig源代码的linter,强制执行编码标准。
安装:
git clone https://github.com/rockorager/ziglint
cd ziglint
zig build install -Doptimize=ReleaseFast --prefix $HOME/.local
使用:
ziglint # 检查当前目录(如果存在.ziglint.zon则使用)
ziglint src build.zig # 检查特定路径
ziglint --ignore Z001 # 抑制特定规则
配置(.ziglint.zon):
.{
.paths = .{ "src", "build.zig" },
.rules = .{
.Z001 = .{ .enabled = false },
.Z024 = .{ .max_length = 80 },
},
}
内联抑制:
fn MyBadName() void {} // ziglint-ignore: Z001