Comprehensive reference for writing performant, idiomatic Zig code in the Marathon project.
- No allocation - Stack variables, comptime, slices of existing data
- FixedBufferAllocator - Pre-sized buffer, no heap, ideal for known bounds
- BoundedArray - Compile-time max size, runtime length, no allocator needed
- ArenaAllocator - Batch allocations, single bulk free at scope end
- GeneralPurposeAllocator - Debug builds, leak detection, safety checks
- SmpAllocator - Maximum performance multithreaded allocation
// Slices: alloc/free
var arr = try allocator.alloc(usize, count);
defer allocator.free(arr);
// Single items: create/destroy
var item = try allocator.create(T);
defer allocator.destroy(item);
// Arena pattern - batch free
var arena = std.heap.ArenaAllocator.init(backing_allocator);
defer arena.deinit();
const aa = arena.allocator();
// Arena reset with retention (for request loops)
defer _ = arena.reset(.{ .retain_with_limit = 8192 });
// Fixed buffer - no heap
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);- Always defer cleanup immediately after allocation
- errdefer for partial cleanup when multiple allocations can fail
- Never double-free - causes crashes
- Track ownership - memory must be freed by code holding reference
- Prefer bufPrint over format when output size is bounded
// errdefer pattern for multiple allocations
var players = try allocator.alloc(Player, count);
errdefer allocator.free(players);
var history = try allocator.alloc(Move, count * 10);
// If history alloc fails, players is freed by errdefer// Return error union
fn process() !Result { ... }
fn mayFail() error{OutOfMemory, InvalidInput}!void { ... }
// Propagate with try
const result = try mayFail();
// Handle with catch
const value = mayFail() catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.InvalidInput => default_value,
};
// Catch with default
const value = mayFail() catch default_value;const FileError = error{ NotFound, PermissionDenied };
const IoError = error{ ReadError, WriteError };
const AllErrors = FileError || IoError;- Use
tryto propagate,catchto handle locally - Never mix
tryandcatchon same expression errdeferfor cleanup on error paths only- Panics don't trigger defer/errdefer
// Compile-time constants
const PI = comptime @acos(-1.0);
const TABLE = comptime generateLookupTable();
// Comptime blocks
const x = comptime blk: {
var result: u32 = 0;
for (0..10) |i| result += i;
break :blk result;
};// Generic struct
fn List(comptime T: type) type {
return struct {
const Self = @This();
items: []T,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Self {
return .{ .items = &.{}, .allocator = allocator };
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.items);
}
};
}
// Generic function
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
// anytype inference
fn print(value: anytype) void {
const T = @TypeOf(value);
// ...
}fn isNumeric(comptime T: type) bool {
return switch (@typeInfo(T)) {
.int, .float, .comptime_int, .comptime_float => true,
else => false,
};
}var list = std.ArrayList(u32).init(allocator);
defer list.deinit();
try list.append(42);
try list.appendSlice(&[_]u32{ 1, 2, 3 });
const slice = list.items;// Auto hash map (most types)
var map = std.AutoHashMap(u32, []const u8).init(allocator);
defer map.deinit();
try map.put(1, "one");
if (map.get(1)) |value| { ... }
// String keys
var smap = std.StringHashMap(u32).init(allocator);
// Iteration
var iter = map.iterator();
while (iter.next()) |entry| {
_ = entry.key_ptr.*;
_ = entry.value_ptr.*;
}// No allocator needed, fixed max size
var buf = std.BoundedArray(u8, 256){};
try buf.append('a');
try buf.appendSlice("hello");
const slice = buf.slice();
// Warning: copies entire array on assignment/function callDebug- Safety checks, no optimizationReleaseSafe- Optimized with safety checksReleaseFast- Maximum speed, minimal safetyReleaseSmall- Optimized for binary size
// Prefer stack allocation
var buf: [1024]u8 = undefined;
// Inline hot functions
inline fn hotPath(x: u32) u32 { ... }
// SIMD vectors
const Vec4 = @Vector(4, f32);
const a: Vec4 = .{ 1, 2, 3, 4 };
const b: Vec4 = .{ 5, 6, 7, 8 };
const c = a + b; // Single SIMD instruction
// Avoid bounds checks in hot loops (when safe)
const ptr = slice.ptr;
for (0..slice.len) |i| {
ptr[i] = value;
}
// Use sentinels to avoid length tracking
const str: [:0]const u8 = "hello";- Heap allocation in hot paths
- Unnecessary copies (BoundedArray, large structs by value)
- Virtual dispatch in tight loops
- Excessive error checking in inner loops
// Slice (fat pointer with length) - preferred
fn process(data: []const u8) void { ... }
// Many-pointer (no length) - use when interfacing with C
fn cProcess(data: [*]const u8, len: usize) void { ... }
// Single pointer
fn modify(ptr: *u32) void { ... }const arr = [_]u8{ 1, 2, 3, 4, 5 };
const slice = arr[1..4]; // [2, 3, 4]
const from_start = arr[0..3]; // [1, 2, 3]
const to_end = arr[2..]; // [3, 4, 5]
// Sentinel-terminated
const str: [:0]const u8 = "hello";const testing = std.testing;
test "example" {
// Use testing allocator for leak detection
var list = std.ArrayList(u32).init(testing.allocator);
defer list.deinit();
try list.append(42);
try testing.expectEqual(@as(u32, 42), list.items[0]);
try testing.expect(list.items.len == 1);
}
// Skip test
test "skip me" {
return error.SkipZigTest;
}const Config = struct {
timeout: u32 = 30,
retries: u8 = 3,
host: []const u8,
};
// Partial initialization with defaults
const cfg: Config = .{ .host = "localhost" };const maybe: ?u32 = getValue();
// orelse for default
const value = maybe orelse 0;
// if for conditional
if (maybe) |v| {
process(v);
}
// .? for unwrap (panics on null)
const value = maybe.?;// Index and value
for (items, 0..) |item, i| { ... }
// Just values
for (items) |item| { ... }
// Just index
for (0..items.len) |i| { ... }