Receber por email
← Voltar para o catálogo

zig-best-practices

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.

367estrelas
Atualizado há 4 meses

Ver no GitHub ↗

Como adicionar

/plugin marketplace add aiskillstore/marketplace

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/zig-best-practices/svg)](https://www.skillteca.com.br/skills/zig-best-practices)

Skills relacionadas

Ver todas de Segurança →

Alerta por categoria

Receba novas skills de Segurança toda segunda

1 email curto, só com as skills novas de Segurança. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

Zig Best Practices

Type-First Development

Types define the contract before implementation. Follow this workflow:

  1. Define data structures - structs, unions, and error sets first
  2. Define function signatures - parameters, return types, and error unions
  3. Implement to satisfy types - let the compiler guide completeness
  4. 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 errdefer for cleanup on error paths; use defer for unconditional cleanup. This prevents resource leaks without try-finally boilerplate.
  • Handle all branches in switch statements; include an else clause that returns an error or uses unreachable for truly impossible cases.
  • Pass allocators explicitly to functions requiring dynamic memory; prefer std.testing.allocator in tests for leak detection.
  • Prefer const over var; prefer slices over raw pointers for bounds safety. Immutability signals intent and enables optimizations.
  • Avoid anytype; prefer explicit comptime T: type parameters. Explicit types document intent and produce clearer error messages.
  • Use std.log.scoped for namespaced logging; define a module-level log constant for consistent scope across the file.
  • Add or update tests for new logic; use std.testing.allocator to 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 defer immediately 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.allocator in 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.scoped to create namespaced loggers; each module should define its own scoped logger for filtering.
  • Define a module-level const log at the top of the file; use it consistently throughout the module.
  • Use appropriate log levels: err for failures, warn for suspicious conditions, info for state changes, debug for 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 comptime parameters 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 @compileError for 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");
    }
Compartilhar X LinkedIn