zig-best-practices
💡 摘要
中文总结。
🎯 适合人群
🤖 AI 吐槽: “看起来很能打,但别让配置把人劝退。”
风险:Medium。建议检查:是否执行 shell/命令行指令;是否发起外网请求(SSRF/数据外发);文件读写范围与路径穿越风险。以最小权限运行,并在生产环境启用前审计代码与依赖。
name: zig-best-practices description: Provides Zig patterns for type-first development with tagged unions, explicit error sets, comptime validation, and memory management. Must use when reading or writing Zig files.
Zig Best Practices
Type-First Development
Types define the contract before implementation. Follow this workflow:
- Define data structures - structs, unions, and error sets first
- Define function signatures - parameters, return types, and error unions
- Implement to satisfy types - let the compiler guide completeness
- Validate at comptime - catch invalid configurations during compilation
Make Illegal States Unrepresentable
Use Zig's type system to prevent invalid states at compile time.
Tagged unions for mutually exclusive states:
// Good: only valid combinations possible 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), } } // Bad: allows invalid combinations const RequestState = struct { loading: bool, data: ?[]const u8, err: ?anyerror, };
Explicit error sets for failure modes:
// Good: documents exactly what can fail const ParseError = error{ InvalidSyntax, UnexpectedToken, EndOfInput, }; fn parse(input: []const u8) ParseError!Ast { // implementation } // Bad: anyerror hides failure modes fn parse(input: []const u8) anyerror!Ast { // implementation }
Distinct types for domain concepts:
// Prevent mixing up IDs of different types const UserId = enum(u64) { _ }; const OrderId = enum(u64) { _ }; fn getUser(id: UserId) !User { // Compiler prevents passing OrderId here } fn createUserId(raw: u64) UserId { return @enumFromInt(raw); }
Comptime validation for invariants:
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, }; }
Non-exhaustive enums for extensibility:
// External enum that may gain variants const Status = enum(u8) { active = 1, inactive = 2, pending = 3, _, }; fn processStatus(status: Status) !void { switch (status) { .active => {}, .inactive => {}, .pending => {}, _ => return error.UnknownStatus, } }
Module Structure
Larger cohesive files are idiomatic in Zig. Keep related code together: tests alongside implementation, comptime generics at file scope, public/private controlled by pub. Split only when a file handles genuinely separate concerns. The standard library demonstrates this pattern with files like std/mem.zig containing 2000+ lines of cohesive memory operations.
Instructions
- Return errors with context using error unions (
!T); every function returns a value or an error. Explicit error sets document failure modes. - Use
errdeferfor cleanup on error paths; usedeferfor unconditional cleanup. This prevents resource leaks without try-finally boilerplate. - Handle all branches in
switchstatements; include anelseclause that returns an error or usesunreachablefor truly impossible cases. - Pass allocators explicitly to functions requiring dynamic memory; prefer
std.testing.allocatorin tests for leak detection. - Prefer
constovervar; prefer slices over raw pointers for bounds safety. Immutability signals intent and enables optimizations. - Avoid
anytype; prefer explicitcomptime T: typeparameters. Explicit types document intent and produce clearer error messages. - Use
std.log.scopedfor namespaced logging; define a module-levellogconstant for consistent scope across the file. - Add or update tests for new logic; use
std.testing.allocatorto catch memory leaks automatically.
Examples
Explicit failure for unimplemented logic:
fn buildWidget(widget_type: []const u8) !Widget { return error.NotImplemented; }
Propagate errors with 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); }
Resource cleanup with errdefer:
fn createResource(allocator: std.mem.Allocator) !*Resource { const resource = try allocator.create(Resource); errdefer allocator.destroy(resource); resource.* = try initializeResource(); return resource; }
Exhaustive switch with explicit default:
fn processStatus(status: Status) ![]const u8 { return switch (status) { .active => "processing", .inactive => "skipped", _ => error.UnhandledStatus, }; }
Testing with memory leak detection:
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); }
Memory Management
- Pass allocators explicitly; never use global state for allocation. Functions declare their allocation needs in parameters.
- Use
deferimmediately after acquiring a resource. Place cleanup logic next to acquisition for clarity. - Prefer arena allocators for temporary allocations; they free everything at once when the arena is destroyed.
- Use
std.testing.allocatorin tests; it reports leaks with stack traces showing allocation origins.
Examples
Allocator as explicit parameter:
fn processData(allocator: std.mem.Allocator, input: []const u8) ![]u8 { const result = try allocator.alloc(u8, input.len * 2); errdefer allocator.free(result); // process input into result return result; }
Arena allocator for batch operations:
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); } // All allocations freed when arena deinits }
Logging
- Use
std.log.scopedto create namespaced loggers; each module should define its own scoped logger for filtering. - Define a module-level
const logat the top of the file; use it consistently throughout the module. - Use appropriate log levels:
errfor failures,warnfor suspicious conditions,infofor state changes,debugfor tracing.
Examples
Scoped logger for a module:
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}); // cleanup }
Multiple scopes in a codebase:
// In src/db.zig const log = std.log.scoped(.db); // In src/http.zig const log = std.log.scoped(.http); // In src/auth.zig const log = std.log.scoped(.auth);
Comptime Patterns
- Use
comptimeparameters for generic functions; type information is available at compile time with zero runtime cost. - Prefer compile-time validation over runtime checks when possible. Catch errors during compilation rather than in production.
- Use
@compileErrorfor invalid configurations that should fail the build.
Examples
Generic function with comptime type:
fn max(comptime T: type, a: T, b: T) T { return if (a > b) a else b; }
Compile-time validation:
fn createBuffer(comptime size: usize) [size]u8 { if (size == 0) { @compileError("buffer size must be greater than 0"); } return [_]u8{0} ** size; }
Avoiding anytype
- Prefer
comptime T: typeoveranytype; explicit type parameters document expected constraints and produce clearer errors. - Use
anytypeonly when the function genuinely accepts any type (likestd.debug.print) or for callbacks/closures. - When using
anytype, add a doc comment describing the expected interface or constraints.
Examples
Prefer explicit comptime type (good):
fn sum(comptime T: type, items: []const T) T { var total: T = 0; for (items) |item| { total += item; } return total; }
Avoid anytype when type is known (bad):
// Unclear what types are valid; error messages will be confusing fn sum(items: anytype) @TypeOf(items[0]) { // ... }
Acceptable anytype for callbacks:
/// Calls `callback` for each item. Callback must accept (T) and return void. fn forEach(comptime T: type, items: []const T, callback: anytype) void { for (items) |item| { callback(item); } }
Using @TypeOf when anytype is necessary:
fn debugPrint(value: anytype) void { const T = @TypeOf(value); if (@typeInfo(T) == .Pointer) { std.debug.print("ptr: {*}\n", .{value}); } else { std.debug.print("val: {}\n", .{value}); } }
Error Handling Patterns
- Define specific error sets for functions; avoid
anyerrorwhen possible. Specific errors document failure modes. - Use
catchwith a block for error recovery or logging; usecatch unreachableonly when errors are truly impossible. - Merge error sets with
||when combining operations that can fail in different ways.
Examples
Specific error set:
const ConfigError = error{ FileNotFound, ParseError, InvalidFor
优点
- 优点1
- 优点2
缺点
- 缺点1
- 缺点2
相关技能
免责声明:本内容来源于 GitHub 开源项目,仅供展示和评分分析使用。
版权归原作者所有 0xBigBoss.
