Lesson 01 — Introduction

What is Zig?

A general-purpose, compiled systems language designed for robustness, optimality, and clarity — without hidden control flow or hidden memory allocations.

Why Zig?

Zig occupies a similar space to C — low-level, manual memory management, no garbage collector — but fixes many of C's pain points:

  • No undefined behavior — Zig makes UB explicit and detectable
  • No hidden allocations — functions that allocate must accept an Allocator
  • No preprocessor — metaprogramming via comptime instead
  • Excellent C interop — can import C headers directly
  • Cross-compilation built-in — target any platform from any platform

Your First Program

zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, Zig!\n", .{});
}
Note @import is a built-in function (all builtins start with @). The standard library is imported as a value — no special syntax required.

Building & Running

shell
# Install Zig from ziglang.org, then:
zig init            # create a new project
zig build run       # build and run
zig run hello.zig   # run a single file
Try it You can experiment with Zig in your browser at zig.godbolt.org or the official zig-play.dev playground — no install needed!
Which symbol prefix identifies Zig built-in functions?
Lesson 02 — Variables & Types

Variables & Types

Zig is statically typed. Every variable has a type known at compile time. Immutability is the default.

const vs var

zig
const x: i32 = 42;     // immutable — cannot be reassigned
var   y: i32 = 10;     // mutable
y = 20;                 // OK
// x = 99;             // ERROR: cannot assign to constant
Zig Philosophy Prefer const whenever possible. Zig will warn you if you declare a var that's never mutated.

Primitive Types

TypeDescriptionExample
i8, i16, i32, i64, i128Signed integers-42
u8, u16, u32, u64, u128Unsigned integers255
f32, f64Floating point3.14
boolBooleantrue / false
usizePointer-sized unsigned intarray indices
comptime_intCompile-time integerliterals

Type Inference

zig
const a = 100;         // inferred as comptime_int
const b: u8 = 100;      // explicit u8
const pi = 3.14159;    // inferred as comptime_float
const name = "Alice";  // type: *const [5:0]u8 (string literal)

Integer Arithmetic

zig
const a: u8 = 200;
const b: u8 = 100;

// Normal addition — panics on overflow in debug mode
const sum = a + b;

// Wrapping arithmetic — always wraps mod 256
const wrapped = a +% b;  // 44 (wraps around)

// Saturating arithmetic
const sat = a +| b;     // 255 (max u8)
Safety In Debug and ReleaseSafe modes, integer overflow causes a panic. Use +% for explicit wrapping semantics.
What keyword declares an immutable variable in Zig?
Lesson 03 — Functions

Functions

Functions in Zig are first-class. They must specify parameter types and return types explicitly. No overloading — use comptime generics instead.

Basic Function

zig
fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    const result = add(3, 4);
    std.debug.print("{d}\n", .{result}); // 7
}

pub makes a declaration visible outside its file. fn without pub is file-private.

Multiple Return Values

Zig doesn't have tuple returns, but you can return a struct:

zig
const DivResult = struct {
    quotient: i32,
    remainder: i32,
};

fn divmod(a: i32, b: i32) DivResult {
    return .{ .quotient = a / b, .remainder = a % b };
}

const r = divmod(17, 5);
// r.quotient == 3, r.remainder == 2

Generic Functions (comptime)

zig
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

const a = max(i32, 10, 20);   // 20
const b = max(f64, 1.5, 0.9); // 1.5
No Overloading Instead of function overloading, Zig uses comptime T: type to create type-generic functions resolved entirely at compile time. Zero runtime cost.
What keyword makes a Zig declaration visible from other files?
Lesson 04 — Control Flow

Control Flow

Zig's control flow is explicit and expression-oriented. if, while, and for can all produce values.

if / else

zig
const x = 42;

// Statement form
if (x > 0) {
    std.debug.print("positive\n", .{});
} else if (x == 0) {
    std.debug.print("zero\n", .{});
} else {
    std.debug.print("negative\n", .{});
}

// Expression form (ternary-like)
const label = if (x > 0) "pos" else "non-pos";

while Loop

zig
var i: u32 = 0;
while (i < 5) : (i += 1) {
    std.debug.print("{d} ", .{i});
}
// prints: 0 1 2 3 4

// while with a continue expression is Zig's "for loop"

for Loop (over ranges & slices)

zig
const nums = [_]i32{ 10, 20, 30 };

// iterate values
for (nums) |n| {
    std.debug.print("{d}\n", .{n});
}

// iterate with index
for (nums, 0..) |n, i| {
    std.debug.print("[{d}] = {d}\n", .{ i, n });
}

// range (Zig 0.12+)
for (0..5) |i| {
    std.debug.print("{d} ", .{i});
}

switch

zig
const n: u8 = 3;
const name = switch (n) {
    1       => "one",
    2, 3    => "two or three",   // multiple values
    4..10   => "four to ten",    // range
    else    => "other",
};
// name == "two or three"
Exhaustive Switches Zig's switch must cover all cases. If you miss a case, the compiler will error — no hidden fallthrough.
In Zig, how do you write a for loop over a range 0 to 4 (inclusive)?
Lesson 05 — Arrays & Slices

Arrays & Slices

Arrays have a fixed, compile-time length. Slices are a pointer + length pair — a view into any contiguous memory.

Arrays

zig
// [N]T — N is the length, T is the element type
const arr: [3]i32 = [3]i32{ 1, 2, 3 };

// Shorthand — let compiler count
const arr2 = [_]i32{ 1, 2, 3 };

std.debug.print("{d}\n", .{arr2[1]});  // 2
std.debug.print("{d}\n", .{arr2.len}); // 3

Slices

zig
const arr = [_]u8{ 10, 20, 30, 40, 50 };

// Slice: arr[start..end]  (end is exclusive)
const s: []const u8 = arr[1..4];
// s == { 20, 30, 40 },  s.len == 3

// Whole array as slice
const all = arr[0..];
// all.len == 5
[]T vs [N]T [N]T is an array of exactly N elements. []T is a slice — a fat pointer (ptr + len) that can point to any array or subrange. Functions should usually accept slices, not arrays, for flexibility.

Strings

Zig has no dedicated string type. Strings are []const u8 (a slice of bytes):

zig
const name: []const u8 = "Alice";
std.debug.print("Hello, {s}!\n", .{name});
std.debug.print("Length: {d}\n", .{name.len}); // 5

// String comparison
const eq = std.mem.eql(u8, name, "Alice"); // true
What is the Zig type for a string literal like "hello" when assigned to a variable?
Lesson 06 — Structs

Structs

Structs group related data together. In Zig, structs can also have methods — functions that take the struct as their first parameter.

Defining a Struct

zig
const Point = struct {
    x: f32,
    y: f32,
};

const p = Point{ .x = 3.0, .y = 4.0 };
std.debug.print("({d}, {d})\n", .{ p.x, p.y });

Struct Methods

zig
const Vec2 = struct {
    x: f32,
    y: f32,

    pub fn length(self: Vec2) f32 {
        return std.math.sqrt(self.x * self.x + self.y * self.y);
    }

    pub fn scale(self: *Vec2, factor: f32) void {
        self.x *= factor;
        self.y *= factor;
    }
};

var v = Vec2{ .x = 3.0, .y = 4.0 };
std.debug.print({d}\n", .{v.length()}); // 5.0
v.scale(2.0);  // v.x=6, v.y=8
self vs *self self: Vec2 — read-only, takes a copy.
self: *Vec2 — mutable reference, can modify the struct.

Default Values

zig
const Config = struct {
    width: u32 = 800,
    height: u32 = 600,
    fullscreen: bool = false,
};

const cfg = Config{ .width = 1920 }; // height & fullscreen use defaults
To write a mutating method on a struct, the receiver type should be:
Lesson 07 — Error Handling

Error Handling

Zig has no exceptions. Errors are values — returned explicitly and handled explicitly. This makes error handling visible and impossible to accidentally ignore.

Error Sets

zig
const ParseError = error{
    InvalidCharacter,
    Overflow,
    Empty,
};

fn parseAge(s: []const u8) ParseError!u8 {
    if (s.len == 0) return error.Empty;
    return std.fmt.parseInt(u8, s, 10) catch error.InvalidCharacter;
}

The ! in ParseError!u8 means "this function returns either a ParseError or a u8".

Handling Errors

zig
// try — propagates errors up (like ? in Rust) fn run() !void { const age = try parseAge("25"); std.debug.print("Age: {d}\n", .{age}); } // catch — handle the error inline const age = parseAge("abc") catch |err| { std.debug.print("Error: {}\n", .{err}); return; }; // catch with default value const age2 = parseAge("bad") catch 0;
try vs catch try expr is shorthand for expr catch |e| return e. Use try to propagate, catch to handle locally.

Anyerror

zig
// !void means "anyerror!void" — can return any error pub fn main() !void { try someFunction(); }
What does try expr do in Zig?
Lesson 08 — Pointers

Pointers

Zig gives you direct control over memory via pointers. Unlike C, Zig's type system distinguishes single-item pointers from many-item pointers and prevents common bugs.

Single-Item Pointer

zig
var x: i32 = 42;
const ptr: *i32 = &x;   // & takes the address

ptr.* = 100;            // .* dereferences
std.debug.print("{d}\n", .{x}); // 100

Pointer Types

TypeMeaning
*TSingle non-null pointer to T
?*TOptional (nullable) pointer to T
[*]TMany-item pointer (unknown length)
[]TSlice: pointer + length
*const TImmutable pointer to T
No Null by Default *T can NEVER be null. Use ?*T to represent a nullable pointer. This eliminates a whole class of null-dereference bugs at compile time.

Heap Allocation

zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit(); // check for leaks on exit
    const alloc = gpa.allocator();

    const n = try alloc.create(i32);
    defer alloc.destroy(n);  // guaranteed cleanup

    n.* = 99;
    std.debug.print("{d}\n", .{n.*});
}
defer defer runs a statement when the current scope exits — like Go's defer or C++'s RAII. It's the idiomatic way to pair allocation with deallocation.
How do you dereference a pointer ptr in Zig?
Lesson 09 — Optionals

Optionals

Zig uses ?T to represent a value that might be absent. This replaces null pointer hacks and makes "maybe nothing" explicit in the type system.

Optional Basics

zig
var maybe: ?i32 = 42;
maybe = null;           // now absent

// Unwrap with .? — panics if null
const val = maybe.?;

// Safe unwrap with orelse
const safe = maybe orelse 0; // 0 if null

// orelse with return/break
const x = maybe orelse return;

if with optional capture

zig
fn findUser(id: u32) ?[]const u8 {
    if (id == 1) return "Alice";
    return null;
}

if (findUser(1)) |name| {
    std.debug.print("Found: {s}\n", .{name});
} else {
    std.debug.print("Not found\n", .{});
}
Payload Capture The |name| syntax captures the unwrapped value inside the if block. This pattern is also used in while loops for iterators that return optionals.

while with optional

zig
// Loops until iterator returns null
while (iterator.next()) |item| {
    process(item);
}
What operator provides a default value when an optional is null?
Lesson 10 — Comptime

Comptime

Zig's most powerful feature: code that runs at compile time. It replaces C macros, C++ templates, and code generation tools — all with regular Zig syntax.

comptime values

zig
// Evaluated at compile time
const SIZE = 1024;
const buf: [SIZE]u8 = undefined;

// Explicit comptime expression
const x = comptime (1 + 2 + 3); // == 6, computed at compile

Generic Data Structures

zig
// A function that RETURNS a type
fn Stack(comptime T: type) type {
    return struct {
        items: []T,
        top: usize = 0,

        pub fn push(self: *@This(), item: T) void {
            self.items[self.top] = item;
            self.top += 1;
        }
    };
}

// Create a Stack of i32
const IntStack = Stack(i32);
Types are Values In Zig, types are comptime values. A function can accept a type and return a type. This is how the standard library's ArrayList(T), HashMap(K, V), etc. work.

comptime if

zig
fn doThing(comptime debug: bool) void {
    if (comptime debug) {
        // This entire branch compiled out in release
        std.debug.print("debug mode\n", .{});
    }
}
No Macro Language Everything in comptime is regular Zig: loops, conditionals, function calls. There's no separate templating syntax to learn.
In Zig, a function that accepts comptime T: type and returns type is used to create:
What's Next? You've covered the core of Zig! Explore further: tagged unions, async/await (experimental), build.zig build system, C interop with @cImport, and the standard library at ziglang.org/documentation.
1 / 10