What is Zig?
Zig is a systems programming language designed to be a "better C" — simpler, safer, and more maintainable while remaining just as fast. It compiles to native code and can interoperate with C libraries seamlessly.
No Hidden Control Flow
No operator overloading, no hidden allocations, no implicit function calls. What you see is what you get.
Compile-Time Execution
Run any function at compile time with comptime. No macros needed — just regular Zig code.
Manual Memory
No garbage collector. You control allocations explicitly with allocator interfaces.
Cross-Compilation
First-class cross-compilation to 50+ targets. Build for any platform from any platform.
Basics
Hello World
const std = @import("std"); pub fn main() void { std.debug.print("Hello, {s}!\n", .{"world"}); }
Variables
// Constants (immutable) const x: i32 = 42; const y = 42; // type inferred // Variables (mutable) var z: i32 = 0; z += 1; // Undefined (uninitialized) var buf: [256]u8 = undefined;
Functions
fn add(a: i32, b: i32) i32 { return a + b; } // Public function (exported) pub fn multiply(a: i32, b: i32) i32 { return a * b; } // Function that can fail fn divide(a: i32, b: i32) !i32 { if (b == 0) return error.DivisionByZero; return @divTrunc(a, b); }
Types
Primitive Types
| Category | Types | Notes |
|---|---|---|
| Signed Integers | i8, i16, i32, i64, i128, isize |
Also arbitrary: i7, i123, etc. |
| Unsigned Integers | u8, u16, u32, u64, u128, usize |
Also arbitrary: u3, u48, etc. |
| Floats | f16, f32, f64, f80, f128 |
IEEE 754 floats |
| Boolean | bool |
true or false |
| Void | void |
Zero-sized type |
| Comptime | comptime_int, comptime_float |
Arbitrary precision at compile time |
Arrays & Slices
// Fixed-size array const arr: [5]i32 = .{ 1, 2, 3, 4, 5 }; // Slice (pointer + length) const slice: []const i32 = arr[1..4]; // {2, 3, 4} // String literals are []const u8 const str: []const u8 = "hello"; // Sentinel-terminated (null-terminated) const c_str: [*:0]const u8 = "hello";
Optionals
// Optional type: can be null var maybe: ?i32 = 42; maybe = null; // Unwrap with orelse const value = maybe orelse 0; // Unwrap with if if (maybe) |val| { std.debug.print("Got: {}\n", .{val}); }
Pointers
var x: i32 = 42; // Single-item pointer const ptr: *i32 = &x; ptr.* = 100; // dereference // Many-item pointer (unknown length) const many: [*]i32 = &arr; // Optional pointer var opt_ptr: ?*i32 = null; opt_ptr = &x;
Control Flow
If / Else
const result = if (x > 0) "positive" else "non-positive"; if (optional_value) |value| { // value is unwrapped here } else { // optional was null }
While
var i: usize = 0; while (i < 10) : (i += 1) { // loop body } // With optional unwrapping while (iterator.next()) |item| { // process item }
For
// Iterate over slice/array for (items) |item| { std.debug.print("{}\n", .{item}); } // With index for (items, 0..) |item, i| { std.debug.print("[{}] = {}\n", .{i, item}); } // Range (0 to 9) for (0..10) |i| { _ = i; }
Switch
const result = switch (x) { 0 => "zero", 1...9 => "single digit", 10, 100 => "ten or hundred", else => "other", }; // Capture value switch (tagged_union) { .some_tag => |value| process(value), else => {}, }
Labeled Blocks
const result = blk: { if (condition) break :blk 42; break :blk 0; };
Error Handling
Zig uses explicit error unions instead of exceptions. Errors are values, not control flow.
Error Unions
// Define error set const FileError = error { NotFound, PermissionDenied, EndOfFile, }; // Function returning error union fn readFile(path: []const u8) FileError![]u8 { if (path.len == 0) return error.NotFound; // ... } // Inferred error set fn process() !void { // ! means inferred error set }
Handling Errors
// try: propagate error up const data = try readFile("config.txt"); // catch: handle error const data = readFile("config.txt") catch |err| { std.debug.print("Error: {}\n", .{err}); return; }; // catch with default value const data = readFile("config.txt") catch "default"; // if-else with error capture if (readFile("config.txt")) |data| { // success } else |err| { // handle error }
defer & errdefer
fn process() !void { const file = try openFile(); defer file.close(); // Always runs on scope exit const buffer = try allocator.alloc(u8, 1024); errdefer allocator.free(buffer); // Only runs on error try riskyOperation(); // If this fails, buffer is freed // If we get here, buffer is NOT freed (success path) }
defer statements execute in reverse order. Use them to ensure cleanup happens even when errors occur.
Memory Management
Zig has no garbage collector. You manage memory explicitly using allocator interfaces.
Allocators
const std = @import("std"); // General purpose allocator (recommended) var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Allocate single item const ptr = try allocator.create(MyStruct); defer allocator.destroy(ptr); // Allocate array/slice const slice = try allocator.alloc(u8, 1024); defer allocator.free(slice);
Common Allocators
- GeneralPurposeAllocator
- Default choice. Tracks leaks in debug mode. Thread-safe.
- ArenaAllocator
- Fast bump allocator. Free everything at once. Great for request handling.
- FixedBufferAllocator
- Allocate from a fixed buffer. No syscalls. Stack-friendly.
- page_allocator
- Direct OS pages. Use for large allocations or as backing for other allocators.
Compile-Time Execution
Zig's comptime lets you run any code at compile time. This replaces macros and enables powerful metaprogramming.
Basic Comptime
// Compute at compile time const factorial = comptime blk: { var result: u64 = 1; for (1..11) |i| { result *= i; } break :blk result; // 3628800 }; // Array size from comptime const size = comptime calculateSize(); var buffer: [size]u8 = undefined;
Generic Functions
fn max(comptime T: type, a: T, b: T) T { return if (a > b) a else b; } const result = max(i32, 5, 10); // 10
Type Reflection
fn printFields(comptime T: type) void { const info = @typeInfo(T); inline for (info.@"struct".fields) |field| { std.debug.print("Field: {s}\n", .{field.name}); } }
Comptime String Operations
// String concatenation (comptime only) const greeting = "Hello, " ++ "World!"; // Array repetition (comptime only) const dashes = "-" ** 40; // "----------------------------------------"
++ and ** operators only work at compile time. For runtime string operations, use std.mem functions.
Structs & More
Structs
const Point = struct { x: f32, y: f32, // Method pub fn distance(self: Point, other: Point) f32 { const dx = self.x - other.x; const dy = self.y - other.y; return @sqrt(dx * dx + dy * dy); } // Associated function (no self) pub fn origin() Point { return .{ .x = 0, .y = 0 }; } }; const p = Point{ .x = 3, .y = 4 }; const dist = p.distance(Point.origin());
Enums
const Color = enum { red, green, blue, pub fn isWarm(self: Color) bool { return self == .red; } }; const c: Color = .green;
Tagged Unions
const Value = union(enum) { int: i64, float: f64, string: []const u8, none, pub fn format(self: Value) []const u8 { return switch (self) { .int => "integer", .float => "float", .string => "string", .none => "none", }; } }; const v = Value{ .int = 42 };
Standard Library Highlights
Common Imports
const std = @import("std"); // Commonly used const mem = std.mem; // Memory utilities const fmt = std.fmt; // Formatting const fs = std.fs; // File system const heap = std.heap; // Allocators const ArrayList = std.ArrayList;
ArrayList
var list = std.ArrayList(i32).init(allocator); defer list.deinit(); try list.append(42); try list.appendSlice(&.{ 1, 2, 3 }); for (list.items) |item| { std.debug.print("{}\n", .{item}); }
HashMap
var map = std.StringHashMap(i32).init(allocator); defer map.deinit(); try map.put("answer", 42); if (map.get("answer")) |value| { std.debug.print("Found: {}\n", .{value}); }
File I/O
// Read entire file const data = try std.fs.cwd().readFileAlloc( allocator, "file.txt", 1024 * 1024, // max size ); defer allocator.free(data); // Write file const file = try std.fs.cwd().createFile("out.txt", .{}); defer file.close(); try file.writeAll("Hello!");
Build System
Quick Commands
# Run directly zig run main.zig # Build executable zig build-exe main.zig # Build with optimizations zig build-exe -O ReleaseFast main.zig # Run tests zig test test.zig # Initialize project zig init
build.zig (Minimal)
const std = @import("std"); pub fn build(b: *std.Build) void { const exe = b.addExecutable(.{ .name = "myapp", .root_source_file = b.path("src/main.zig"), .target = b.standardTargetOptions(.{}), .optimize = b.standardOptimizeOption(.{}), }); b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); }
Cross-Compilation
# Build for Windows from Linux/macOS zig build -Dtarget=x86_64-windows # Build for Linux from anywhere zig build -Dtarget=x86_64-linux-gnu # Build for macOS zig build -Dtarget=aarch64-macos