Notes on Zig ⚡¶
Quick reference for Zig, which is a small, explicit system programming language.
The 0.16.0 release added I/O as an Interface (std.Io) and rewires main to take a std.process.Init.
No hidden control flow, no hidden allocations, no preprocessor; comptime replaces macros and generics; errors are values; allocators
are explicit and passed in.
Each section has a core idiom; expand the More examples blocks for extended snippets.
Hello World and Toolchain¶
Compile a single file, scaffold a project, run tests: the three things you do constantly.
const std = @import("std");
pub fn main(init: std.process.Init) !void {
try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, world!\n");
std.debug.print("formatted: {d}\n", .{42}); // Debug.print still works (stderr)
}
More examples
| Command | Effect |
|---|---|
zig run hello.zig |
Build and run in one step. |
zig build-exe hello.zig -O ReleaseFast |
Optimized native binary. |
zig build-exe hello.zig -target x86_64-linux-musl |
Cross-compile to musl Linux. |
zig init |
Scaffold build.zig + src/main.zig. |
zig build run |
Build and run via build.zig. |
zig test file.zig |
Run all test "..." {} blocks. |
zig fmt src/ |
Auto-format sources. |
zig env |
Show toolchain paths and target info. |
// In 0.16.0, I/O is an interface; pass an Io and a buffer to get a Writer.
var buf: [256]u8 = undefined;
var w = std.Io.File.stdout().writer(init.io, &buf);
try w.interface.print("value={d}\n", .{42});
try w.interface.flush();
// Quick stderr debug print (no buffer or Io needed):
std.debug.print("err: {s}\n", .{msg});
Variables, Constants, and Literals¶
const by default; var only when you need to mutate. Numeric literals are untyped until used.
const pi: f64 = 3.14159; // Immutable and typed
var count: u32 = 0; // Mutable
count += 1;
const inferred = 42; // Type comptime_int; coerces to any int
const hex = 0xFF; const bin = 0b1010; const oct = 0o17;
const million = 1_000_000; // Underscores allowed
More examples
Primitive Types¶
Arbitrary-width integers, explicit numeric conversions, and a few special types you’ll meet early.
| Family | Examples | Notes |
|---|---|---|
| Signed int | i8 i16 i32 i64 i128 isize |
Plus arbitrary iN up to i65535. |
| Unsigned int | u8 u16 u32 u64 u128 usize |
usize is pointer-sized. |
| Float | f16 f32 f64 f80 f128 |
IEEE-754. |
| Bool, Unit | bool, void, noreturn |
noreturn for fns that never return. |
| Meta | type, anytype, anyopaque |
Used in generics and FFI. |
| String | []const u8, [*:0]const u8 |
Slices and null-terminated for C. |
More examples
const a: i32 = 100;
const b: i64 = a; // Widening: implicit
const c: i16 = @intCast(a); // Narrowing: explicit, panic on overflow
const d: u32 = @bitCast(@as(i32, -1)); // Reinterpret bits
const e: f32 = @floatFromInt(a);
const f: i32 = @intFromFloat(3.7); // Truncates toward zero
const g: u8 = @truncate(0x1FF); // Keeps low 8 bits
Control Flow¶
Everything is an expression: if, switch, blocks, even loops yield values.
if (x > 0) print("pos") else if (x == 0) print("zero") else print("neg");
var i: u32 = 0;
while (i < 10) : (i += 1) print("{d}\n", .{i});
for (items, 0..) |item, idx| print("{d}: {}\n", .{ idx, item });
const label = switch (color) {
.red, .pink => "warm",
.blue, .cyan => "cool",
else => "other",
};
More examples
Optionals¶
No null pointers in Zig. ?T is the only way to express “maybe absent”, and the compiler forces you to handle
it.
var maybe: ?u32 = null;
maybe = 7;
if (maybe) |val| print("got {}\n", .{val})
else print("none\n", .{});
const x = maybe orelse 0; // Default
const y = maybe.?; // Unwrap, panic on null
More examples
Errors¶
Errors are values from an error set. ! in a return type means “may also return one of these
errors”.
const ParseError = error{ Empty, Invalid };
fn parse(s: []const u8) ParseError!u32 {
if (s.len == 0) return error.Empty;
return std.fmt.parseInt(u32, s, 10) catch return error.Invalid;
}
const n = try parse("42"); // Propagate
const m = parse("x") catch 0; // Default
parse("x") catch |err| log(err); // Inspect
More examples
const NetError = error{ Timeout, Refused };
const AnyError = ParseError || NetError;
fn fetchAndParse() AnyError!u32 { ... }
Functions¶
Plain, generic, or duck-typed via anytype. The first parameter convention for methods is self.
fn add(a: i32, b: i32) i32 { return a + b; }
pub fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
fn log(comptime fmt: []const u8, args: anytype) void {
std.debug.print(fmt, args);
}
More examples
Arrays, Slices, and Pointers¶
Arrays have a known length in the type; slices are pointer + length; pointers come in single-item, many-item, and slice flavors.
const arr = [_]i32{ 1, 2, 3, 4 }; // Type [4]i32
const slice: []const i32 = arr[1..3]; // {2, 3}
const len = slice.len; // Runtime length
var x: i32 = 10;
const p: *i32 = &x; // Single-item pointer
p.* = 20;
More examples
Structs and Methods¶
Structs are types. Methods are just functions with a self parameter declared inside the struct.
const Vec2 = struct {
x: f32,
y: f32,
pub fn init(x: f32, y: f32) Vec2 {
return .{ .x = x, .y = y };
}
pub fn length(self: Vec2) f32 {
return @sqrt(self.x * self.x + self.y * self.y);
}
};
const v = Vec2.init(3, 4);
const n = v.length(); // 5
More examples
const Config = struct {
retries: u32 = 3, // Default
verbose: bool = false,
};
// Packed: bit-precise layout, useful for protocols
const Header = packed struct {
flag: u1,
kind: u3,
len: u12,
};
// Extern: C ABI for interop with C structs
const CTimespec = extern struct { sec: i64, nsec: i64 };
Enums and Tagged Unions¶
Enums are integers with names. union(enum) is a discriminated union: the foundation for sum types and ASTs.
const Color = enum { red, green, blue };
const c: Color = .red; // `.` infers enum type from context
const Shape = union(enum) {
circle: f32,
rect: struct { w: f32, h: f32 },
point,
};
switch (s) {
.circle => |r| ...,
.rect => |r| ...,
.point => ...,
}
More examples
Allocators¶
Memory is explicit: every allocation takes an Allocator. Pick the right one for the job and pair every alloc
with a free (or use an arena).
const std = @import("std");
pub fn main(init: std.process.Init) !void {
// Either use the pre-set-up gpa from init...
const alloc = init.gpa;
// ...or build your own DebugAllocator (the GPA successor).
var dbg = std.heap.DebugAllocator(.{}){};
defer _ = dbg.deinit(); // Reports leaks
_ = dbg.allocator();
const buf = try alloc.alloc(u8, 1024);
defer alloc.free(buf);
// ArrayList is now unmanaged: init with .empty, pass allocator each call.
var list: std.ArrayList(u32) = .empty;
defer list.deinit(alloc);
try list.append(alloc, 42);
}
More examples
| Allocator | Use when… |
|---|---|
DebugAllocator |
App-wide, long-lived; debug mode catches leaks and double-frees. (Was GeneralPurposeAllocator in 0.15; alias still works.) |
ArenaAllocator |
Many short-lived allocations; free everything in one shot. |
FixedBufferAllocator |
You have a stack or static buffer and want zero heap. |
page_allocator |
Small, infrequent, large allocations. |
c_allocator |
Mixing with C code that expects malloc/free. |
std.testing.allocator |
Inside test blocks; fails the test on leak. |
Comptime and Generics¶
Run regular Zig at compile time. Types are values, so generics are just functions returning types.
fn List(comptime T: type) type {
return struct {
items: []T,
len: usize,
};
}
const IntList = List(i32);
comptime {
const n = 10 * 10;
@compileLog("computed at compile time", n);
}
More examples
Defer and Errdefer¶
Cleanup that runs on every exit (defer) or only on the error path (errdefer). Indispensable with
allocators and resources.
fn work(alloc: std.mem.Allocator) ![]u8 {
const buf = try alloc.alloc(u8, 64);
errdefer alloc.free(buf); // Only on error
try doSomething(buf); // If this fails, buf is freed
return buf; // Success: caller owns it
}
defer file.close(); // Runs on scope exit, always
More examples
Builtins and Casts¶
Built-in functions start with @. They cover reflection, casting, intrinsics, and module loading.
| Group | Builtins |
|---|---|
| Modules | @import, @cImport, @cInclude, @embedFile |
| Casts | @as, @intCast, @floatCast, @ptrCast, @bitCast, @truncate |
| Conversions | @intFromFloat, @floatFromInt, @intFromEnum, @enumFromInt, @intFromBool |
| Reflection | @sizeOf, @alignOf, @typeInfo, @TypeOf, @typeName, @hasDecl, @hasField |
| Math and SIMD | @min, @max, @abs, @sqrt, @mod, @rem, @splat, @reduce, @shuffle |
| Memory | @memcpy, @memset, @addrOf |
| Diagnostics | @panic, @compileError, @compileLog, @errorName |
Build System (build.zig)¶
A real Zig program defines its build graph in build.zig. Cross-compilation, optimization modes, and steps are
first-class.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run = b.addRunArtifact(exe);
if (b.args) |args| run.addArgs(args);
const step = b.step("run", "Run the app");
step.dependOn(&run.step);
}
More examples
const tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_tests = b.addRunArtifact(tests);
b.step("test", "Run unit tests").dependOn(&run_tests.step);
// Add a dependency declared in build.zig.zon
const dep = b.dependency("zap", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zap", dep.module("zap")); // The root_module is now built via b.createModule
| Optimize mode | Behavior |
|---|---|
Debug |
Runtime safety on, no optimization. Default. |
ReleaseSafe |
Optimized, runtime safety still on. |
ReleaseFast |
Optimized, safety off. Smallest, fastest, riskiest. |
ReleaseSmall |
Size-optimized, safety off. |
Testing¶
Tests are first-class: write test "name" { ... } next to the code, run with zig test or zig
build test.
const std = @import("std");
const testing = std.testing;
test "basic add" {
try testing.expectEqual(4, 2 + 2);
}
test "slice equal" {
try testing.expectEqualSlices(u8, "hi", "hi");
}
More examples
Threads and Atomics¶
Async/await was removed in 0.11; the current concurrency story is OS threads, mutexes, channels (in user libs), and atomics.
const std = @import("std");
fn worker(id: u32) void {
std.debug.print("worker {}\n", .{id});
}
pub fn main() !void {
const t = try std.Thread.spawn(.{}, worker, .{1});
t.join();
}
More examples
C Interop¶
First-class. Translate headers with @cImport, link C with -lc, declare extern fn
for plain symbols.
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
_ = c.printf("hello from C\n");
}
More examples
Std Library Tour¶
The standard library is large but discoverable; here are the modules you’ll touch most.
| Module | What it has |
|---|---|
std.fmt |
format, parseInt, parseFloat, bufPrint, allocPrint. |
std.mem |
Slice ops: eql, indexOf, split, tokenize, copy, swap. |
std.fs |
cwd(), openFileAbsolute, Dir.iterate, File.reader/writer. |
std.io |
Generic readers and writers, buffered I/O, fixed-buffer streams. |
std.ArrayList(T) |
Growable array, takes an allocator. |
std.AutoHashMap(K,V), StringHashMap |
Hash maps with sensible defaults. |
std.json |
Parse + stringify, with reflection-driven (de)serialize. |
std.http |
HTTP client and basic server. |
std.crypto |
Hashes, MACs, ciphers, KDFs, X25519, Ed25519. |
std.process |
Args, env, child processes, exit codes. |
std.os |
POSIX and syscall layer (per-target). |
std.time |
Monotonic and wall clocks, sleep, timers. |
std.log |
Leveled logging configurable per scope. |
std.testing |
Assertions and the leak-checking allocator. |
Idiomatic snippets
const data = try std.fs.cwd().readFileAlloc(alloc, "input.txt", 10 * 1024 * 1024);
defer alloc.free(data);
Common Gotchas¶
- unused: Unused variables, imports, and parameters are compile errors. Discard with
_ =. - slices: A slice borrows; storing one outliving its backing storage is undefined behavior.
- undefined:
= undefinedmeans uninitialized memory; reading before writing is UB in release builds. - comptime: Functions that take
comptime T: typemust be called with a comptime-known type. Usually fine, but watch for hot loops. - stdin:
std.io.getStdIn().reader().readUntilDelimiter…requires a buffer you own; nothing is allocated implicitly. - error: Don’t silently
catch unreachablefor errors that can happen; usecatch |e|with a real branch.