Zig Basics
Zig language fundamentals for systems programming including comptime, error handling, and manual memory management
You are an expert in the Zig programming language for writing systems-level code.
## Key Points
- Always use `defer` and `errdefer` for resource cleanup immediately after acquisition.
- Pass allocators explicitly rather than relying on global state.
- Use `comptime` to move work from runtime to compile time when inputs are known.
- Prefer slices (`[]T`) over raw pointers (`[*]T`) for bounds-checked access.
- Use `std.testing` and `zig test` for unit tests colocated with source code.
- Leverage `@import("builtin")` to query target architecture for cross-compilation.
- Use `std.log` with scoped loggers instead of `std.debug.print` in library code.
- Forgetting `defer` for cleanup leads to resource leaks; always pair acquisition with deferred release.
- Returning a pointer to stack memory produces a dangling pointer; allocate on the heap or return by value.
- Ignoring error returns silently discards failures; use `try`, `catch`, or explicitly discard with `_ = expr`.
- Mixing up `[]T` (slice) and `[*]T` (many-pointer) causes subtle bugs at C boundaries.
- Using the wrong allocator lifetime (e.g., arena freed too early) leads to use-after-free.
## Quick Example
```zig
fn process() !void {
const data = try readFile("config.txt");
defer allocator.free(data);
try parseConfig(data);
}
```skilldb get systems-programming-skills/Zig BasicsFull skill: 237 linesZig Basics — Systems Programming
You are an expert in the Zig programming language for writing systems-level code.
Core Philosophy
Overview
Zig is a systems programming language designed as a practical alternative to C, offering manual memory management, comptime metaprogramming, and first-class interop with C/C++ libraries. It targets the same use cases as C but with improved safety, readability, and maintainability. Zig has no hidden control flow, no hidden allocations, and no garbage collector.
Core Concepts
Comptime (Compile-Time Evaluation)
Zig's comptime keyword forces expressions to be evaluated at compile time, replacing the need for macros or template metaprogramming.
fn fibonacci(comptime n: u32) u32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Evaluated entirely at compile time
const fib_10 = fibonacci(10); // 55
Comptime also enables generic programming:
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
const Self = @This();
pub fn get(self: Self, index: usize) T {
return self.items[index];
}
};
}
const IntList = List(i32);
Error Handling
Zig uses error unions instead of exceptions. Errors are values that can be returned from any function.
const FileError = error{
NotFound,
PermissionDenied,
IoError,
};
fn readFile(path: []const u8) FileError![]u8 {
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
return switch (err) {
error.FileNotFound => FileError.NotFound,
error.AccessDenied => FileError.PermissionDenied,
else => FileError.IoError,
};
};
defer file.close();
return file.readToEndAlloc(allocator, max_size) catch FileError.IoError;
}
The try keyword propagates errors concisely:
fn process() !void {
const data = try readFile("config.txt");
defer allocator.free(data);
try parseConfig(data);
}
Manual Memory Management with Allocators
Zig does not have a default allocator. Every allocation is explicit and tied to an allocator interface.
const std = @import("std");
pub fn main() !void {
// General purpose allocator with safety checks in debug mode
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.appendSlice("hello");
}
Arena allocator for batch allocations:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// All allocations freed at once when arena is deinitialized
const buf1 = try allocator.alloc(u8, 1024);
const buf2 = try allocator.alloc(u8, 2048);
// No need to free individually
Slices and Pointers
// Slices are a pointer + length pair
fn sum(items: []const i32) i64 {
var total: i64 = 0;
for (items) |item| {
total += item;
}
return total;
}
// Sentinel-terminated pointers for C interop
const c_string: [*:0]const u8 = "hello";
Packed Structs and Bit Manipulation
const Flags = packed struct {
readable: bool,
writable: bool,
executable: bool,
_padding: u5 = 0,
};
const flags = Flags{ .readable = true, .writable = false, .executable = true };
const raw: u8 = @bitCast(flags); // 0b00000101
Implementation Patterns
Tagged Unions for State Machines
const Token = union(enum) {
identifier: []const u8,
number: i64,
plus,
minus,
eof,
pub fn format(self: Token, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
switch (self) {
.identifier => |name| try writer.print("ident({s})", .{name}),
.number => |n| try writer.print("num({})", .{n}),
.plus => try writer.writeAll("+"),
.minus => try writer.writeAll("-"),
.eof => try writer.writeAll("EOF"),
}
}
};
Defer and Errdefer for Resource Cleanup
fn connectAndQuery(url: []const u8) !Result {
const conn = try openConnection(url);
errdefer conn.close(); // Only runs if function returns an error
const stmt = try conn.prepare("SELECT * FROM users");
errdefer stmt.deinit();
const result = try stmt.execute();
// On success, caller owns conn and result
return result;
}
Async I/O (Event Loop)
const std = @import("std");
fn fetchUrl(url: []const u8) ![]u8 {
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
var req = try client.request(.GET, try std.Uri.parse(url), .{}, .{});
defer req.deinit();
try req.start();
try req.wait();
return try req.reader().readAllAlloc(allocator, 1 << 20);
}
Best Practices
- Always use
deferanderrdeferfor resource cleanup immediately after acquisition. - Pass allocators explicitly rather than relying on global state.
- Use
comptimeto move work from runtime to compile time when inputs are known. - Prefer slices (
[]T) over raw pointers ([*]T) for bounds-checked access. - Use
std.testingandzig testfor unit tests colocated with source code. - Leverage
@import("builtin")to query target architecture for cross-compilation. - Use
std.logwith scoped loggers instead ofstd.debug.printin library code.
Common Pitfalls
- Forgetting
deferfor cleanup leads to resource leaks; always pair acquisition with deferred release. - Returning a pointer to stack memory produces a dangling pointer; allocate on the heap or return by value.
- Ignoring error returns silently discards failures; use
try,catch, or explicitly discard with_ = expr. - Mixing up
[]T(slice) and[*]T(many-pointer) causes subtle bugs at C boundaries. - Using the wrong allocator lifetime (e.g., arena freed too early) leads to use-after-free.
- Comptime functions that accidentally depend on runtime values cause confusing compile errors.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add systems-programming-skills
Related Skills
Build Systems
Build systems for systems programming including CMake, Meson, and Zig build with cross-compilation
Cpp Modern
Modern C++ (C++20/23) patterns including concepts, ranges, coroutines, and modules
Ffi
Foreign function interfaces for calling between C, C++, Rust, Zig, Python, and other languages safely
Memory Safety
Techniques for achieving memory safety without garbage collection including ownership models, arenas, and static analysis
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.
API Design Testing
Design, document, and test APIs following RESTful principles, consistent