Zig — Reference
Source: https://ziglang.org/documentation/master/
Zig
- Created: announced 2016-02-08 by Andrew Kelley (Wikipedia).
- Latest stable: Zig 0.16.0, released 2026-04-13 (ziglang.org/download). Zig is pre-1.0; breaking changes still happen between releases.
- Owner: Zig Software Foundation (501(c)(3) non-profit, since 2020). License: MIT.
- Paradigms: imperative, procedural, low-level systems; some functional flavor; metaprogramming via
comptime. - Typing: static, strong, nominal; type inference where unambiguous; types are first-class comptime values.
- Memory: manual; no GC, no hidden allocations, no destructors. Allocators are explicit, passed-in values. Optionals (
?T) and error unions (E!T) handle absence/failure. - Compilation: AOT to native via self-hosted compiler (LLVM backend, with WIP self-hosted x86_64/aarch64 backends).
zig ccis a drop-in C/C++ cross-compiler. - Primary domains: systems programming, embedded, game engines, replacing C, cross-compilation toolchain, WebAssembly. Bun runtime is the most visible Zig user.
- Official docs: https://ziglang.org/documentation/master/
At a glance
Zig is a small, explicit, low-level language designed by people who got tired of C’s footguns and C++‘s complexity but didn’t want a garbage collector. The language has no preprocessor, no macros, no operator overloading, no exceptions, no hidden control flow, and no hidden allocations. The metaprogramming story is comptime — running normal Zig code at compile time on types as values. The build system, package manager, C compiler, and cross-compiler are all the same zig binary; you can target any supported triple from any host without installing anything else.
Getting started
Install: download tarball from https://ziglang.org/download/ (no system install needed — just put on PATH). Package managers: brew install zig, pacman -S zig, winget install zig.zig. Zig versions are specific: pin via your project’s build.zig.zon (minimum_zig_version).
Version manager: zigup (community, recommended) or zvm. Many projects pin a nightly via build.zig.zon.
Hello world (file hello.zig):
const std = @import("std");
pub fn main() !void {
try std.io.getStdOut().writer().print("Hello, world!\n", .{});
}Run: zig run hello.zig. Build executable: zig build-exe hello.zig.
Project layout:
myproj/
build.zig # build script (Zig code)
build.zig.zon # manifest: name, version, deps, paths
src/
main.zig
root.zig
zig-out/ # build outputs (gitignored)
.zig-cache/ # incremental cache (gitignored)
Initialize with zig init (creates exe + lib scaffold).
Build / package tool: zig build runs build.zig. There is no separate package manager — build.zig.zon lists dependencies (URL + hash); zig build fetches and verifies them. Per-project cache in .zig-cache/, global cache in ~/.cache/zig/.
REPL: there is no official REPL. zig run file.zig is the script-style replacement.
Basics
Types and literals:
- Integers:
i8/i16/i32/i64/i128,u8/u16/...u128, plus arbitrary widthsi7,u23,u1(a bit).usize/isizeare pointer-sized. - Floats:
f16,f32,f64,f80,f128. bool,void,noreturn,anyopaque(C’svoid *).comptime_int,comptime_float— unbounded literal types only valid at compile time.- Composites: arrays
[N]T(length is part of the type), slices[]T(pointer + length),struct,enum,union,error,?T(optional),E!T(error union). - Pointers:
*T(single),[*]T(many, no length),[*:0]T(sentinel-terminated, e.g. C strings),*const T. - Strings: string literals are
*const [N:0]u8— sentinel-terminatedu8arrays.
Variables/scoping: const x = 1; (immutable, default), var x: i32 = 1; (mutable). Block-scoped. const and var at file scope are globals. All declarations require a value or undefined — uninitialized is var x: i32 = undefined;, which is a contract that you’ll write before reading.
Control flow: if, else, while, for, switch, defer, errdefer, unreachable, break, continue, return. for (slice) |item, i| iterates with optional index. switch is exhaustive on enums and produces a value.
Functions:
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn divide(a: i32, b: i32) !i32 { // !T means "error union"
if (b == 0) return error.DivByZero;
return @divTrunc(a, b);
}
pub fn main() !void {
const r = try divide(10, 2); // try = unwrap or propagate err
_ = r;
}No closures (functions can’t capture runtime state — but comptime data is accessible). Anonymous functions exist; they cannot capture.
Strings: not a special type — they are []const u8 (UTF-8 bytes, by convention). No automatic UTF-8 handling in the stdlib’s basic indexing; std.unicode provides utilities. Concatenation is allocation: std.mem.concat(allocator, u8, &.{ "a", "b" }).
Collections: std.ArrayList(T), std.AutoHashMap(K, V), std.StringHashMap(V), std.MultiArrayList(T) (struct-of-arrays), std.SegmentedList(T, prealloc), std.PriorityQueue(T, Ctx, lessThan). All take an allocator at construction.
Intermediate
Type system depth: nominal, static. Generics are not a separate feature — they’re functions returning types at comptime:
fn ArrayList(comptime T: type) type {
return struct {
items: []T,
capacity: usize,
// methods...
};
}
const IntList = ArrayList(i32);Tagged unions: union(enum) { int: i32, str: []const u8 } — switch-checked exhaustively.
Modules: each .zig file is a struct (an implicit container). @import("path") returns that struct. There is no import x as y syntax — assign the result: const json = @import("std").json;. Packages defined in build.zig get aliases the build system makes importable.
Error handling: errors are values, not exceptions. An error set is an enum-like type:
const FileError = error{ NotFound, AccessDenied };
fn open(path: []const u8) FileError!File { ... }E!T is the error union. Operators:
try expr— propagate error up.catch— handle:const x = open("f") catch |err| return err;catch unreachable— assert no error (panic in safe modes).errdefer— defer that only runs on the error path.
Concurrency primitives: as of 0.16.0, stackful coroutine async/await was removed in 0.11 and is being redesigned; do not write new async code yet. Today: OS threads via std.Thread.spawn, std.Thread.Mutex, std.atomic, std.Thread.Pool. Single-threaded event loops via std.io.poll. The io_uring and epoll/kqueue story is currently library-level (Tigerbeetle’s io.zig, libxev).
I/O: std.io provides Reader and Writer interfaces (vtable-based abstractions). std.fs for filesystem (cwd().openFile, createFile, readFileAlloc). std.net for sockets. std.process for subprocess + env vars. std.posix for raw syscalls.
Stdlib highlights: std.mem (memory ops, allocators), std.fmt (formatting), std.heap (allocators: GeneralPurposeAllocator, ArenaAllocator, FixedBufferAllocator, c_allocator, page_allocator), std.testing (expect, expectEqual), std.json, std.crypto, std.compress, std.hash, std.simd, std.Build.
Advanced
Memory model: there is no GC. Every allocation is explicit and goes through an Allocator interface (a vtable struct). Common patterns:
- Arena:
std.heap.ArenaAllocator— bulk-free everything at end (great for request-scoped). - GeneralPurposeAllocator (GPA): leak-detecting, double-free-detecting; the dev default.
- FixedBufferAllocator: fail when fixed slab is full; great for hard real-time and embedded.
c_allocator: wrapsmalloc/free.page_allocator: directmmap/VirtualAlloc.
Allocator is passed in to every function that allocates. This makes resource discipline auditable from the call site.
Concurrency deep dive: as of 0.16, the recommended approach for parallelism is OS threads (std.Thread, std.Thread.Pool). For I/O concurrency, libraries like libxev provide a cross-platform event loop. Stackless async is being reworked (the Andrew Kelley July 2024 post outlines Io as an interface — see ziglang.org blog).
FFI: best-in-class.
@cImport({ @cInclude("stdio.h"); })literally parses C headers and exposes them as Zig declarations. (Translation via translate-c, the same engine aszig translate-c.)- Calling C: just call the resulting function. ABIs match
extern fn. - Exporting to C:
export fn my_fn(x: c_int) c_int { ... }— visible asmy_fnsymbol. - Linking C source:
b.addCSourceFiles(...)inbuild.zig. zig ccis a C compiler (clang frontend, Zig’s improved cross-compilation):zig cc -target aarch64-linux-musl hello.c.
Reflection: @TypeOf(x), @typeInfo(T) — full structural introspection at comptime (returns a tagged union describing fields, decls, params, etc.). Build generic JSON serializers, pretty-printers, ORMs entirely at compile time.
Performance tools:
zig build -Doptimize=ReleaseFast(max perf) /ReleaseSafe(kept safety checks) /ReleaseSmall/Debug(default).--summary allprints build steps + cache hit info.perf(Linux), Instruments (macOS), VTune.@setRuntimeSafety(false)disables UB checks per scope.- Inline assembly:
asm volatile (...)with named operands. @prefetch,@branchHint(.likely),@setFloatMode(.optimized).- Built-in benchmarking:
std.time.Timer.
God mode
comptime is the single big idea. Mark a parameter or block comptime and Zig evaluates it during compilation:
fn factorial(comptime n: u32) u32 {
var r: u32 = 1;
inline for (1..n + 1) |i| r *= @intCast(i);
return r;
}
const six = factorial(3); // computed at compile time
const tbl: [factorial(5) + 1]u32 = ...; // type uses comptime resultUse comptime for:
- Generics: functions that take
comptime T: typeand build a struct around it. - Conditional compilation:
if (builtin.os.tag == .windows) .... - Compile-time tables: SIMD lookup tables, regex compilation, JSON schema validation.
- DSLs: parse a format string at compile time to a typed parser.
inline for/inline while— fully unroll, allowing different types per iteration.@compileError("message")to fail compilation on bad input.
@import / @cImport: @import("std"), @import("./util.zig"). @cImport is a block of C preprocessor directives whose result is a Zig namespace.
Vector types: @Vector(4, f32) is a SIMD vector; arithmetic operators apply lanewise. @reduce, @shuffle, @select for cross-lane ops.
Inline assembly: full LLVM-style inline asm with typed input/output operand bindings.
build.zig is Zig: no DSL, no YAML. The build system is a Zig program that builds a graph of Steps. Cross-compile any project for any target by changing one line:
const target = b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl });Cross-compilation everywhere: zig build -Dtarget=x86_64-windows-gnu works from a Mac or Linux box without any toolchain install. Same for zig cc -target .... This is unique among general-purpose toolchains.
Allocators as a first-class concept: every library that allocates takes an Allocator parameter. There is no global heap. This makes test-time allocators trivial (std.testing.allocator detects leaks) and embedded use possible (pass a FixedBufferAllocator).
Async/await transform (legacy / pre-0.11; being redesigned): the compiler used to perform a stackless coroutine transform — turning an async function into a state machine struct with a frame() method. The new design under discussion exposes I/O as an interface (std.Io) so async-ness is a property of the I/O implementation, not the function color.
Zig as a C compiler: zig cc and zig c++ use Zig’s bundled clang frontend with libcs (musl, glibc multi-version, MSVCRT, mingw, wasi-libc, macos-libc) bundled. Famously used as a portable, easily cross-targetable build of clang.
Idioms & style
- Naming:
lowerCamelCasefor functions and variables;TitleCasefor types;SCREAMING_SNAKE_CASErarely (often justTitleCasefor constants too); file namessnake_case.zig. - Formatter:
zig fmtis built into the toolchain; no config, no opinions to argue. - Linter: no separate linter — the compiler’s safety modes and
comptimechecking cover most ground.zlint(community) exists. - Idiomatic patterns:
tryearly,errdeferfor cleanup on error path.- Pass allocator first param to constructor; keep it on the struct.
- Prefer slices
[]Tover many-pointers[*]T. - Use tagged unions + exhaustive
switchfor variants. - Use
comptimeto monomorphize, not runtime polymorphism. - Single-file modules; export via top-level
pubdecls.
- Expert review focus: allocator correctness (leaks, double-free, use-after-free in Debug/ReleaseSafe), error set granularity (avoid
anyerror),undefinedwrites before reads, integer overflow (+%/-%are wrap operators), pointer aliasing innoaliasparameters, comptime explosion (build-time cost), C ABI struct layout when usingextern.
Ecosystem
- HTTP / web:
std.http(built-in client + server),zap(mongoose-based),httpz. - Game / graphics:
zig-gamedev(mach-glfw, zmath, zaudio), Mach engine, raylib bindings, sokol bindings. - Database:
pg.zig(Postgres),zqlite(SQLite wrapper),zigzag(Redis). - Parsing / serialization:
std.json,zig-yaml,std.compress.gzip,tres(TOML). - CLI:
zig-cli,zig-clap. - Testing:
std.testingis built-in (test "name" { ... }blocks; run withzig test file.zigorzig build test). No third-party framework needed;expect,expectEqual,expectError,expectEqualSlices. - Docs:
zig build-exe -femit-docsgenerates static HTML from doc comments (//!for module,///for decl). Style: https://ziglang.org/documentation/master/std/. - Notable users: Bun (JS runtime, Zig is the implementation language), TigerBeetle (financial DB), Uber (zig cc as their cross-compile toolchain at scale), Roc lang compiler, Mach engine, Ghostty terminal.
Gotchas
- Pre-1.0: APIs change between minor versions. Always pin the compiler version in
build.zig.zon. async/awaitis currently absent (removed 0.11, being redesigned). Don’t write async-style code yet.undefinedis a contract you make: writing it is fine, reading it is UB. In Debug/ReleaseSafe, reads are filled with0xAAto make bugs loud.- Integer overflow in
+/-/*is UB inReleaseFast, panics in Debug/ReleaseSafe. Use+%(wrap),+|(saturate),@addWithOverflow. - Slice lifetimes: a slice of a stack array becomes invalid when the stack frame returns. Borrow-checker doesn’t exist; you must reason yourself.
@cImportof two files with the same C header doesn’t dedupe types — combine all headers into one@cImportblock.- Allocator forgetting: failing to
defer alloc.free(x)is a leak the GPA will report on shutdown — but only if you initialize the GPA correctly with.deinit(). - Pointer aliasing under
noalias: violating the contract is UB. comptimerecursion limits can cripple large compile-time computations; tune@setEvalBranchQuota.- String == comparison doesn’t work:
[]const u8is a slice; usestd.mem.eql(u8, a, b). - No method syntax for free functions:
obj.method(arg)only works ifmethodis declared inside the type’s struct (or extension via@field). - Build cache footguns: editing
build.zigdoesn’t always invalidate downstream.rm -rf .zig-cache zig-outif things get weird. - Zig stdlib is unstable: nightly often breaks
std.io.Writer,std.Build.Step, etc. Track release notes.
Citations
- Zig Language Reference (master): https://ziglang.org/documentation/master/
- Zig stdlib: https://ziglang.org/documentation/master/std/
- Downloads (versions, dates, platforms): https://ziglang.org/download/
- ZigLearn (community guide): https://ziglearn.org/
- Zig Software Foundation: https://ziglang.org/zsf/
- Zig in-depth blog (Andrew Kelley) on
Ioand async future: https://ziglang.org/news/ zig ccoverview: https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.htmlbuild.zigguide: https://ziglang.org/learn/build-system/zig initand project layout: https://ziglang.org/learn/getting-started/- Bun (largest Zig user): https://bun.sh/
- TigerBeetle (Zig database): https://tigerbeetle.com/
- Wikipedia (history, license): https://en.wikipedia.org/wiki/Zig_(programming_language)