May 21, 2026
Concepts
- Memory is a giant array of boxes, each with an address
- Stack, automatic, fast, limited. Variables live here. Gone when function ends.
- Heap, manual, flexible, large. You ask for it, you free it.
- Stack holds pointers into the heap, the key in your pocket, the locker in the hold
- Pointers, variables that hold memory addresses, not values.
&x gives address, x.* dereferences - Function pointers, functions are also just bytes at an address.
&greet works. - Everything is bytes, strings, numbers, code, all of it
- Uninitialized memory, boxes the OS gives you aren’t cleaned. Whatever was there before is what you get.
- ASLR (Address Space Layout Randomization), security feature, stack gets placed in a different memory address each time, making it harder for attackers to predict where things are in memory.
Zig specifics
@import built-in compiler function, @ prefix means compiler built-inconst vs var default to const, use var only when you need mutation!void function returns nothing or an errortry attempt something that could fail, propagate error up if it doesdefer run this when scope exits, no matter what. Write cleanup next to setup.DebugAllocator heap allocator that tracks leaks in debug mode_ = explicitly discard a return value{s} for strings, {d} for integers, {f} for floats in printfor (items) |item| loop with capture syntax
Program #1: I just came to say hello
const std = @import("std");
pub fn main() void {
std.debug.print("Hello, Zig!\n", .{});
}
Program #2: Exploring pointers and dereferencing
const std = @import("std");
pub fn main() void {
var age: i32 = 8;
const ptr = &age;
std.debug.print("age is: {}\n", .{age});
std.debug.print("address of age: {}\n", .{ptr});
std.debug.print("value at address: {}\n", .{ptr.*});
}
Program #3: Allocating memory on the heap
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const status = gpa.deinit();
if (status == .leak) @panic("memory leak!");
}
const allocator = gpa.allocator();
const numbers = try allocator.alloc(i32, 5);
defer allocator.free(numbers);
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;
for (numbers) |n| {
std.debug.print("{d}\n", .{n});
}
}
May 23, 2026
Concepts
- A slice is a pointer + a length. Not just “a section of an array.” It can point into any chunk of memory.
[5]i32 array, size fixed at compile time/[]i32 slice, size flexible.numbers[1..4] range syntax, end is exclusive (1, 2, 3, not 4)..len built into every slice and array.- Nothing gets copied when you slice. Same memory, different view.
Program #4: Arrays and slices
const std = @import("std");
pub fn main() void {
var numbers = [5]i32{10, 20, 30, 40, 50};
const slice = numbers[1..4];
std.debug.print("full array length: {}\n", .{numbers.len});
std.debug.print("slice length: {}\n", .{slice.len});
for (slice) |n| {
std.debug.print("{d}\n", .{n});
}
}
May 25, 2026
Concepts
!returntype function might fail. !i32 = integer or error. !void = nothing or error.error.SomeName how you create and signal an errortry propagate error up to caller if it failscatch |err| handle the error right here, capture it in err- Zig will not let you silently ignore errors
or logical OR for conditions| bitwise OR, operates on actual bits. Not the same as or@divTrunc integer division rounded toward zero@divFloor integer division rounded toward negative infinity@divExact panics if division isn’t exact
Program #5: Functions with slice parameters
const std = @import("std");
fn sum(numbers: []const i32) i32 {
var total: i32 = 0;
for (numbers) |n| {
total += n;
}
return total;
}
pub fn main() void {
const nums = [5]i32{10, 20, 30, 40, 50};
const result = sum(&nums);
std.debug.print("sum is: {d}\n", .{result});
}
Program #6: Error handling
const std = @import("std");
fn divide(a: i32, b: i32) !i32 {
if (b == 0) return error.DivisionByZero;
if (a > 1000 or b > 1000) return error.TooBig;
return @divTrunc(a, b);
}
pub fn main() !void {
const a: i32 = 22;
const b: i32 = 3;
const result = divide(a, b) catch |err| {
std.debug.print("the system is down: {}\n", .{err});
return;
};
std.debug.print("{} divided by {} is: {}\n", .{ a, b, result });
}
May 25, 2026
Concepts
- A struct groups related data into one custom type
- Struct names are capitalized by convention
[]const u8 is how Zig represents strings, a slice of bytes you won’t modify- A string has no special type in Zig, it’s just bytes. “A” is just the number 65.
self: Kid method gets a copy. Reads data, can’t mutate original.self: *Kid method gets a pointer. Can mutate the original.- Two things can be immutable independently, the variable itself, and the data it points to
const on a variable = can’t reassign the variableconst inside []const u8 = can’t modify the bytes being pointed to- Structs can have methods that live inside the struct definition
- Zig won’t let you declare
var if you never mutate it, use const - Types must always be
const. var Kid = struct won’t compile
Program #7: Structs with methods
const std = @import("std");
pub fn main() void {
var kid1 = Kid{
.name = "CATS",
.age = 11,
.grade = 6,
};
const kid2 = Kid{
.name = "Captain",
.age = 10,
.grade = 5,
};
const kids = [2]Kid{ kid1, kid2 };
for (kids) |k| {
k.describe();
}
std.debug.print("{s} was {}\n", .{ kid1.name, kid1.age });
kid1.birthday();
std.debug.print("Happy birthday, {s}! you're now {}!\n", .{ kid1.name, kid1.age });
}
const Kid = struct {
name: []const u8,
age: i32,
grade: i32,
pub fn describe(self: Kid) void {
std.debug.print("{s} is {d} years old and in grade {d}\n", .{ self.name, self.age, self.grade });
}
pub fn birthday(self: *Kid) void {
self.age += 1;
}
};
May 26, 2026
Concepts
- Null pointer bugs are one of the most common crashes in software history. Zig eliminates them with optionals.
?type means this value might exist or might be null. ?[]const u8, ?i32, ?Kid it works on any type.- Without
? Zig won’t let you assign null. You can’t accidentally have null where you didn’t expect it. if (value) |unwrapped| safely unwrap an optional. Only runs if value exists.orelse provide a fallback. value orelse "default" returns the value or the default.orelse return if value is null, exit the function immediately.orelse unreachable tells Zig “this will never be null.” Panics in debug mode if you’re wrong.
Program #8: Hey Sailor (structs, optionals, pointers)
const std = @import("std");
const print = std.debug.print;
const Sailor = struct {
name: []const u8,
age: i32,
rank: i32,
nickname: ?[]const u8 = null,
pub fn describe(s: Sailor) void {
print("{s} is {} years old and an E-{}. ", .{ s.name, s.age, s.rank });
const nick = s.nickname orelse "no known nickname";
print("Nickname: {s}.\n", .{nick});
}
pub fn promote(s: *Sailor) void {
s.rank += 1;
print("{s} was promoted to E-{}!\n", .{ s.name, s.rank });
}
};
pub fn main() void {
var sailor1 = Sailor{
.name = "Jimmy Butta",
.age = 19,
.rank = 1,
.nickname = "Sandy",
};
var sailor2 = Sailor{
.name = "Billy Knives",
.age = 17,
.rank = 2,
};
const sailors = [2]*Sailor{ &sailor1, &sailor2 };
for (sailors) |sailor| {
sailor.describe();
}
sailor1.promote();
sailor2.promote();
}
May 26, 2026
Concepts
std.process.Init in 0.16, main can accept this struct. Zig pre-builds it before the program starts. Contains gpa, arena, io, environment variables, and args.init.minimal.args where command line arguments live inside Init.iterate() creates an iterator from Args. An iterator is a playhead, Args is the playlist..next() get the next item, or null if done. Used in a while loop.args[0] is always the binary path in every language. Your actual args start at index 1._ = iter.next() skip an argument by discarding ititer.next() orelse return get next arg or exit if nonestd.mem.splitScalar(u8, string, ' ') split a string on a character, returns an iterator-- separates zig run arguments from program’s arguments- Iterators can’t be indexed like arrays. Must walk them with
.next() ArenaAllocator allocate a bunch of stuff, free it all at once with deinit(). Like a whiteboard you erase in one wipe. Perfect for short-lived data.- When docs fail,
grep the source grep "pub fn" /path/to/file.zig shows all available functions
Skill: Reading source when docs fail
# See what's inside a struct
grep -A 20 "pub const Init" /home/rrb/.zvm/0.16.0/lib/std/process.zig
# See all functions available on a type
grep "pub fn" /home/rrb/.zvm/0.16.0/lib/std/process/Args.zig
Program #9: Word counter CLI (first version)
const std = @import("std");
pub fn main(init: std.process.Init) void {
var iter = init.minimal.args.iterate();
_ = iter.next();
const sentence = iter.next() orelse return;
var count: usize = 0;
var words = std.mem.splitScalar(u8, sentence, ' ');
while (words.next()) |word| {
_ = word;
count += 1;
}
std.debug.print("{d} words\n", .{count});
}
Usage
zig run 09-wordcount.zig -- "take off every zig"
4 words
May 30, 2026
Concepts
- Pass a value means function gets a copy, original untouched
- Pass a pointer means function gets the address, changes hit the original
- orelse with a block is used when you need to do more than one thing on the fallback path
const val = something orelse {
std.debug.print("error message\n", .{});
return;
};
- orelse is an operator, not a statement. You use it, not “call” it.
- Refactoring is reorganizing working code without breaking it.
- Flag variables is when you use a bool to track whether something happened, check it after the fact
Program #9: Word counter CLI (second version)
const std = @import("std");
pub fn main(init: std.process.Init) void {
var iter = init.minimal.args.iterate();
_ = iter.next();
var had_args = false;
var count: usize = 0;
while (iter.next()) |sentence| {
had_args = true;
var words = std.mem.splitScalar(u8, sentence, ' ');
while (words.next()) |word| {
_ = word;
count += 1;
}
}
if (!had_args) {
std.debug.print("usage: wordcount -- \"your sentence here\"\n", .{});
return;
}
std.debug.print("{d} words\n", .{count});
}
Usage
zig run 09-wordcount.zig -- "take off every zig"
4 words
zig run 09-wordcount.zig -- take off every zig
4 words
zig run 09-wordcount.zig
usage: wordcount -- "your sentence here"
May 30, 2026
Concepts
- File descriptor the OS’s way of tracking open files. Just a number representing an open file. When you open a file you get back a handle like
.{ .handle = 3 }. std.Io.Dir.cwd() current working directory. Starting point for opening files.std.Io.File.Reader.init(file, io, buffer) creates a file reader connected to the file. Must be var not const.r.interface the Io.Reader interface on the file reader. Access it directly through r, don’t copy it into a separate variable.r.interface.allocRemaining(gpa, .unlimited) reads entire file contents into heap memory..unlimited is an enum value of Io.Limit. Use .unlimited for reading whole files.- Buffer a fixed chunk of stack memory used as working space for the reader.
var buffer: [4096]u8 = undefined undefined tells Zig “I’ll fill this in later.” Uninitialized memory, same concept as seen with the heap – whatever bytes were there before.std.mem.splitScalar(u8, contents, '\n') split file contents on newlines, same pattern as splitting on spaces in word counter- Pattern: open, read, process open the file, read contents into memory, process the contents. The structure of every file processing program.
Program #10: Line counter CLI
const std = @import("std");
pub fn main(init: std.process.Init) !void {
var iter = init.minimal.args.iterate();
_ = iter.next();
const filename = iter.next() orelse return;
const file = try std.Io.Dir.cwd().openFile(
init.io,
filename,
.{},
);
defer file.close(init.io);
var buffer: [4096]u8 = undefined;
var r = std.Io.File.Reader.init(file, init.io, &buffer);
const contents = try r.interface.allocRemaining(init.gpa, .unlimited);
defer init.gpa.free(contents);
var lines = std.mem.splitScalar(u8, contents, '\n');
var count: usize = 0;
while (lines.next()) |line| {
_ = line;
count += 1;
}
std.debug.print("{d} lines\n", .{count});
}
Usage
zig run 10-linecounter.zig -- warnings.csv
39 lines
May 31, 2026
Concepts
std.mem.eql(u8, a, b) compare slice contents in Zig. == compares pointers, not bytes. Always use eql for string comparison.- Nested iteration split on
\n to get rows, then split each row on , to get fields. One iterator inside another. - Early return as control flow
return inside a loop when match is found. If it falls out of the loop without returning, that’s a not-found case. No flag variable needed. orelse continue unwrap an optional inside a loop. If null, skip to next iteration.orelse with a block when the fallback path needs more than one statement, use a block. orelse { print(...); return; }.return error.SomeName signal failure to the caller. Pairs with a print for human-readable output.- Implicit header skip a header row like
"code" will never match a numeric input, so it gets skipped naturally without explicit logic. void vs error signal return std.debug.print(...) compiles under !void but silently swallows the error. Always print then return error.X separately.
Program #11: Weather warning lookup CLI
const std = @import("std");
pub fn main(init: std.process.Init) !void {
var iter = init.minimal.args.iterate();
_ = iter.next();
const warning_code = iter.next() orelse {
std.debug.print("usage: warnings <weather_code>\n", .{});
return;
};
const filename = "warnings.csv";
const file = try std.Io.Dir.cwd().openFile(
init.io,
filename,
.{},
);
defer file.close(init.io);
var buffer: [4096]u8 = undefined;
var reader = std.Io.File.Reader.init(file, init.io, &buffer);
const max_file_size: i32 = 1024 * 1024;
const contents = try reader.interface.allocRemaining(init.gpa, .limited(max_file_size));
defer init.gpa.free(contents);
var rows = std.mem.splitScalar(u8, contents, '\n');
while (rows.next()) |row| {
var fields = std.mem.splitScalar(u8, row, ',');
const code = fields.next();
const warning = fields.next();
if (std.mem.eql(u8, code orelse continue, warning_code)) {
std.debug.print("warning: {?s}\n", .{warning});
return;
}
}
std.debug.print("invalid warning code: {s}\n", .{warning_code});
return error.CodeNotFound;
}
Usage
zig run 11-lookup-file.zig -- 10
warning: Heavy Rain Advisory
zig run 11-lookup-file.zig -- 99
invalid warning code: 99
error: CodeNotFound
zig run 11-lookup-file.zig
usage: warnings <weather_code>
June 08, 2026
Concepts
@embedFile("file") bakes a file’s contents directly into the binary at compile time. Returns []const u8. No file I/O, no allocator, no runtime file dependency. Good for data that rarely changes.- Data segment where embedded and string literal data lives in the binary. Loaded into memory by the OS at startup. Already there, already constant.
@embedFile vs file I/O embed when data is stable and you want a single self-contained binary. File I/O when data changes without recompiling.[_]Type{} array literal with inferred length. Zig counts the elements for us. []Type is a slice type, not an array literal.- Array of structs as a lookup table a natural pattern for fixed mappings. Iterate with
for, compare with std.mem.eql, return on match. - Struct literals in arrays
.{ .field = value } syntax inside the array, Zig infers the type from context.
Program 12: Weather warning lookup via embedded CSV
const std = @import("std");
pub fn main(init: std.process.Init) !void {
var iter = init.minimal.args.iterate();
_ = iter.next();
const warning_code = iter.next() orelse {
std.debug.print("usage: warnings <weather_code>\n", .{});
return;
};
const warnings_csv = @embedFile("warnings.csv");
var rows = std.mem.splitScalar(u8, warnings_csv, '\n');
while (rows.next()) |row| {
var fields = std.mem.splitScalar(u8, row, ',');
const code = fields.next();
const warning = fields.next();
if (std.mem.eql(u8, code orelse continue, warning_code)) {
std.debug.print("warning: {?s}\n", .{warning});
return;
}
}
std.debug.print("invalid warning code: {s}\n", .{warning_code});
return error.CodeNotFound;
}
Usage
zig run 12-lookup-embed.zig -- 10
warning: Heavy Rain Advisory
zig run 12-lookup-embed.zig -- 99
invalid warning code: 99
error: CodeNotFound
zig run 12-lookup-embed.zig
usage: warnings <weather_code>
Program 13: Weather warning lookup via hardcoded struct array
const std = @import("std");
const Warning = struct {
code: []const u8,
description: []const u8,
pub fn describe(self: Warning) void {
std.debug.print("warning: {s}\n", .{self.description});
}
};
const warnings = [_]Warning{
.{ .code = "0", .description = "Cancelled" },
.{ .code = "2", .description = "Blizzard Warning" },
.{ .code = "3", .description = "Heavy Rain Warning" },
.{ .code = "4", .description = "Flood Warning" },
.{ .code = "5", .description = "Storm Warning" },
.{ .code = "6", .description = "Heavy Snow Warning" },
.{ .code = "7", .description = "High Wave Warning" },
.{ .code = "8", .description = "Storm Surge Warning" },
.{ .code = "9", .description = "Landslide Warning (L3)" },
.{ .code = "10", .description = "Heavy Rain Advisory" },
.{ .code = "12", .description = "Heavy Snow Advisory" },
.{ .code = "13", .description = "Wind & Snow Advisory" },
.{ .code = "14", .description = "Thunder Advisory" },
.{ .code = "15", .description = "Strong Wind Advisory" },
.{ .code = "16", .description = "High Wave Advisory" },
.{ .code = "17", .description = "Snowmelt Advisory" },
.{ .code = "18", .description = "Flood Advisory" },
.{ .code = "19", .description = "Storm Surge Advisory" },
.{ .code = "20", .description = "Dense Fog Advisory" },
.{ .code = "21", .description = "Dry Air Advisory" },
.{ .code = "22", .description = "Avalanche Advisory" },
.{ .code = "23", .description = "Low Temperature Advisory" },
.{ .code = "24", .description = "Frost Advisory" },
.{ .code = "25", .description = "Icing Advisory" },
.{ .code = "26", .description = "Snow Accretion Advisory" },
.{ .code = "27", .description = "Other Advisory" },
.{ .code = "29", .description = "Landslide Advisory (L2)" },
.{ .code = "32", .description = "Blizzard Emergency Warning" },
.{ .code = "33", .description = "Heavy Rain Emergency Warning" },
.{ .code = "35", .description = "Storm Emergency Warning" },
.{ .code = "36", .description = "Heavy Snow Emergency Warning" },
.{ .code = "37", .description = "High Wave Emergency Warning" },
.{ .code = "38", .description = "Storm Surge Emergency Warning" },
.{ .code = "39", .description = "Landslide Emergency Warning (L5)" },
.{ .code = "43", .description = "Heavy Rain Danger Warning (L4)" },
.{ .code = "48", .description = "Storm Surge Danger Warning (L4)" },
.{ .code = "49", .description = "Landslide Danger Warning (L4)" },
};
pub fn main(init: std.process.Init) !void {
var iter = init.minimal.args.iterate();
_ = iter.next();
const warning_code = iter.next() orelse {
std.debug.print("usage: warnings <weather_code>\n", .{});
return;
};
for (warnings) |w| {
if (std.mem.eql(u8, w.code, warning_code)) {
w.describe();
return;
}
}
std.debug.print("invalid warning code: {s}\n", .{warning_code});
return error.CodeNotFound;
}
Usage
zig run 13-lookup-table.zig -- 10
warning: Heavy Rain Advisory
zig run 13-lookup-table.zig -- 99
invalid warning code: 99
error: CodeNotFound
zig run 13-lookup-table.zig
usage: warnings <weather_code>
June 08, 2026
Concepts
std.http.Client Zig’s built-in HTTP client. Needs an allocator and io to initialize. Always defer client.deinit().std.Io.Writer.Allocating a growable buffer that collects the response body into heap memory. Initialize with .init(allocator), always defer response_body.deinit().client.fetch(.{}) performs the HTTP request. Takes a options struct with .method, .location, and .response_writer. Returns a result containing the HTTP status..location = .{ .url = url } how you pass the URL to fetch.response_body.written() returns the collected response bytes as []u8 after the fetch completes.{s} for response body the body is []u8, a byte slice. Use {s} to print as readable text, otherwise Zig won’t know how to format it.- HTTP result vs response body
client.fetch returns a status result. The actual body lives separately in the Io.Writer.Allocating buffer.
Program 14: HTTP client, fetch and print raw response
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
var client = std.http.Client{ .allocator = allocator, .io = io };
defer client.deinit();
const area_url = "https://www.jma.go.jp/bosai/common/const/area.json";
var response_body: std.Io.Writer.Allocating = .init(allocator);
defer response_body.deinit();
const response = try client.fetch(.{
.method = .GET,
.location = .{ .url = area_url },
.response_writer = &response_body.writer,
});
std.debug.print("response status: {}\n", .{response});
const body = response_body.written();
std.debug.print("response body: {s}\n", .{body});
}
Usage
zig run 14-http-client.zig
response status: .{ .status = .ok }
response body: { ...raw JSON... }
June 14, 2026
Concepts
std.json.parseFromSlice(T, allocator, bytes, options) parses a JSON byte slice into a Zig type T. Returns a Parsed(T) wrapper, always defer parsed.deinit()..ignore_unknown_fields = true tells the parser to skip JSON fields that don’t appear in our Zig type. Without this, any extra field in the JSON causes a parse error. Lets us define only the parts of the JSON we care about.std.json.ArrayHashMap(T) represents a JSON object (key-value map) where keys are strings and values are type T. Used when the JSON has unpredictable/dynamic keys, like office codes..map.iterator() walks an ArrayHashMap. entry.key_ptr.* gives the key (dereferenced), entry.value_ptr gives a pointer to the value struct, access fields directly e.g. entry.value_ptr.name.- Defining types for JSON shape every field we want to read must be declared in a matching Zig struct with matching types. The struct mirrors the JSON shape exactly for the fields we care about.
- Exploring JSON shape with
jq curl -s <url> | jq 'keys' shows top-level fields. jq '.offices | to_entries | .[0]' shows one example entry, useful for designing our Zig types before writing them. parsed.value the parsed data, typed as T. Access nested fields normally, e.g. parsed.value.offices.
Program 15: Fetch and parse area.json, print all offices
const std = @import("std");
const Office = struct {
name: []const u8,
enName: []const u8,
officeName: []const u8,
parent: []const u8,
children: []const []const u8,
};
const AreaJson = struct {
offices: std.json.ArrayHashMap(Office),
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const io = init.io;
var client = std.http.Client{ .allocator = allocator, .io = io };
defer client.deinit();
const area_url = "https://www.jma.go.jp/bosai/common/const/area.json";
var response_body: std.Io.Writer.Allocating = .init(allocator);
defer response_body.deinit();
const response = try client.fetch(.{
.method = .GET,
.location = .{ .url = area_url },
.response_writer = &response_body.writer,
});
if (response.status != .ok) {
std.debug.print("HTTP error {}\n", .{response.status});
return;
}
const parsed = try std.json.parseFromSlice(
AreaJson,
allocator,
response_body.written(),
.{ .ignore_unknown_fields = true },
);
defer parsed.deinit();
var iter = parsed.value.offices.map.iterator();
while (iter.next()) |entry| {
std.debug.print("{s} {s} ({s})\n", .{
entry.key_ptr.*,
entry.value_ptr.name,
entry.value_ptr.enName,
});
}
}
Usage
zig run 15-area-offices.zig
011000 宗谷地方 (Soya)
012000 上川・留萌地方 (Kamikawa Rumoi)
...
474000 八重山地方 (Yaeyama)