Zig Cheatsheet

2026-06-16 2026-06-19849 Words

Language

useful built-in functions

The built-in functions begin with @.

import

const std = @import("std");

bitReverse(interge: anytype) T

mathematical operation

// @sqrt @sin @cos @exp @log @floor @addWithOverflow

type casting operation

// @as @errorFromInt @floatFromInt @ptrFromInt @intFromPtr @intFromEnum @intCast

function call

// @call(modifier: std.lang.CallModifier, function: anytype, args: anytype) anytype
// const CallModifier = enum {
//   auto,
//   never_tail,
//   never_inline,
//   no_suspend,
//   always_tail,
//   always_inline,
//   compile_time,
// }

string literal

more details refer: https://ziglang.org/documentation/master/#String-Literals-and-Unicode-Code-Point-Literals

// @embedFile(comptime path: []const u8) *const [N:0]u8

debug tools

// @compileError(comptime msg: []const u8) noreturn

// @compileLog(...) void
// This function prints the arguments passed to it at compile-time.

// @src() std.lang.SourceLocation

arrays

The variables is mutable default, unless declare with const.

var foo: [3]u32 = [3]u32{32, 150, 468}; // {} is initialized list
const bar = foo[2];                     // get values of an array using array[index] notation
foo[0] = 58;                            // in-place changes
const length = foo.len;                 // .len property

concatenate

use ++ to concatenate two arrays.

const a = [_]u8{1};
const b = [_]u8{2};
const c = a ++ b;               // {1, 2}

repeat

use ** to repeat an array.

const d = [_]u8{1, 2, 3} ** 2;  // {1, 2, 3, 1, 2, 3}

strings

single quotes for individual characters and strings use double quotes and not interchangeable.

const ziggy = "hello";
const lyrics =
    \\Ziggy played guitar
    \\and Spidders from Mars
;                               // use \\ for multi-line strings

NOTE: string literals are immutable(const) array of u8 characters at all.

control flow

if

NOTE: condition only accepts boolean values. it won't coerce numbers or other types of data to true and false!

if (condition) {

} else {

}
const discount = true;
const price: u8 = if (discount) 17 else 20; // if statements are also valid expressions!

Use if error clause to handle "error union":

const MyNumberError = error {
    TooBig,
    TooSmall,
};

fn canFail() MyNumberError!u8 {}

pub fn main() void {
    const n = canFail();
    if (n) |value| {
        // normal thing
    } else |err| switch (err) {
        // handle error
    }
}

while

// while (condition): (continue expression) {}

for

TIPs: could loop multiple different items at once.

// for (items) |item| {}
// for (items, 0..) |item, index| {}
// for (items1, items2, items3) |item1, item2, item3|  {}

switch

TIPs: switch statement could be used as an expression. NOTE: switch statement must be exhaustive, or add an else clause to handle defaults.

const players = 2;
switch (players) {
    1 => hello(),
    2 => work(),
    else => {
        alert();
    },
}

functions

fn foo(n: u8) u8 {}             // default is private
pub fn bar(n: u8) u8 {}

defer execute

we can defer execute a block of code with defer keyword. Commonly used for resources cleanup or release.

// {
//   defer runLater();
//   runNow();
// }

errors

const MyNumberError = error {
    TooBig,
    TooSmall,
};

error union

Zig lets us make "error union" which is a value which could either be a regular value OR an error from a set.

var text: MyError!Text = getText("foo.txt");

ignore error with catch

// catch error and replace it with a default value
const foo = canFail() catch 5;  // if fails, foo will be assigned 5

catch then handle

// calFail() catch |err| {
//   if (err == MyError.TooBig) {
//   }
// }

NOTE: we could use errdefer for special handle before the block of code exit when error occurs.

// {
//   errdefer cleanup();
//   try canFail();
// }

bubble up

// try canFail(); => canFail() catch |err| return err;

primative type

enum(internal is number)

const Ops = enum {inc, pow, dec};
// Since enum is really a set of numbers, we can assign them explicitly
const Stuff = enum(u8) {foo = 15}; // default value
const my_stuff: u8 = @intFromEnum(Stuff.foo); // get number from enum

struct

const Character = struct {
    health: u8,
    gold: u32,
    experience: u32,
};

pointer

var foo: u8 = 5;
var bar: *u8 = &foo;
type comment
u8 u8 value
foo the value of 5
*u8 the type of a pointer to a u8 value
&foo a reference to foo
bar a pointer to the value at foo
bar.* the value of 5 (dereference)
different pointer
const foo: u8 = 5;
var bar: u8 = 10;

const p1: *const u8 = &foo;     // const pointer to const value, so p1 is a const readonly pointer
// error: cannot assign to constant
// p1 = &bar;
// p1.* = 10;

var p2: *const u8 = &foo;       // pointer to const value, so p2 is a mutable but pointer to a readonly value
// p2 = &bar;                   // Ok
// error: cannot assign to constant
// p2.* = 10;

const p3: *u8 = &foo;           // though p3 declare as a pointer to u8, but foo declare as const, so it still can't be changed
// error: cannot assign to constant
// p3.* = 10;

optional

var foo: ?u32 = 10;             // it could be u32 or null
var bar = foo orelse 2;         // if foo == null; then bar = 2;
const bzz = bar.?;              // bar orelse unreachable;

no value

undefined

undefined tells the compiler that we are not assigning a value yet.

null

truely no value, typically used with optional type.

error

special value indicate that the value we were looking for does not exist.

void

type not a value. which is a Zero bit types.

slices

slices is a pointer with a length property.

var card = [4]u8{1, 2, 3, 4};
const hand: []u8 = card[0..3];  // slice to first 3 card

union

the compiler sets enough memory for the largest thing that needs.

const Foo = union {
    small: u8,
    large: u64,
};
tagged union

a union which used like a enum;

const Foo = union(enum) {
    small: u8,
    large: u64,
};

coercion rules

types can always be made more restrictive

var foo: u8 = 5;
var p1: *u8 = &foo;
var p2: *const u8 = p1;         // mutable to immutable

numeric types can coerce to larger types

var n1: u8 = 5;
var n2: u16 = n1;

var n3: f16 = 45.2;
var n4: f32 = n3;

single-item pointers to arrays coerce to slices and many-item pointers

const arr: [3]u8 = [3]u8{1, 2, 3};
const s: []const u8 = &arr;     // pointer to slice
const p: [*]const u8 = &arr;

single-item mutable pointers can coerce to single-item pointers pointing to an array of length 1

var five: u8 = 5;
var a_five: *[1]u8 = &five;

payload types and null coerce to optionals

var num: u8 = 6;
var maybe_num: ?u8 = num;
maybe_num = null;

labels

we could custom label for a block of code, which like loop body.

// my_label: {}

// outer_block: while(true) {
//    break :outer_block;
// }

// const bar: u8 = two_loop: while (true) {
//    while (true) {
//        break :two_loop 5;
//    }
// }

comptime

All numeric literals in Zig are of type comptime_int or comptime_float. For that declare with var which could be changed at runtime, the compiler must know exactly how much memory to reserve for these value, so we need to specify numeric types with specific sizes.

var var_int: u32 = 123456;      // u32 must be declare or something like this
comptime var var_int2 = 123456; // or declare as comptime

comptime type (generic programming)

// fn foo(comptime MyType: type) void {}
fn makeSequence(comptime T: type, comptime size: usize) [size]T {
    var sequence: [size]T = undefined;
    var i: usize = 0;

    while (i < size): (i += 1) {
        sequence[i] = @as(T, @intCast(i)) + 1;
    }

    return sequence;
}

comptime value (static dispatch)

Above is a example generic for Type(T), but what if we want to have generic for value?

The answer is anytype placeholder, which tells Zig to infer the actual type of a parameter at compile time.

// fn foo(thing: anytype) void {}

Then we can use builtins such as @TypeOf(), @typeInfo(), @typeName(), @hasDecl(), and @hasField() to determine more about the type. All of this logic will be performed entirely at compile time.

const Duck = struct {
    eggs: u8,
    loudness: u8,
    location_x: i32 = 0,
    location_y: i32 = 0,

    fn waddle(self: *Duck, x: i16, y: i16) void {
        self.location_x += x;
        self.location_y += y;
    }

    fn quack(self: Duck) void {
        if (self.loudness < 4) {
            print("\"Quack.\" ", .{});
        } else {
            print("\"QUACK!\" ", .{});
        }
    }
};

const RubberDuck = struct {
    in_bath: bool = false,
    location_x: i32 = 0,
    location_y: i32 = 0,

    fn waddle(self: *RubberDuck, x: i16, y: i16) void {
        self.location_x += x;
        self.location_y += y;
    }

    fn quack(self: RubberDuck) void {
        // Assigning an expression to '_' allows us to safely
        // "use" the value while also ignoring it.
        _ = self;
        print("\"Squeek!\" ", .{});
    }

    fn listen(self: RubberDuck, dev_talk: []const u8) void {
        // Listen to developer talk about programming problem.
        // Silently contemplate problem. Emit helpful sound.
        _ = dev_talk;
        self.quack();
    }
};

const Duct = struct {
    diameter: u32,
    length: u32,
    galvanized: bool,
    connection: ?*Duct = null,

    fn connect(self: *Duct, other: *Duct) !void {
        if (self.diameter == other.diameter) {
            self.connection = other;
        } else {
            return DuctError.UnmatchedDiameters;
        }
    }
};

const DuctError = error{UnmatchedDiameters};

pub fn main() void {
    // This is a real duck!
    const ducky1 = Duck{
        .eggs = 0,
        .loudness = 3,
    };

    // This is not a real duck, but it has quack() and waddle()
    // abilities, so it's still a "duck".
    const ducky2 = RubberDuck{
        .in_bath = false,
    };

    // This is not even remotely a duck.
    const ducky3 = Duct{
        .diameter = 17,
        .length = 165,
        .galvanized = true,
    };

    print("ducky1: {}, ", .{isADuck(ducky1)});
    print("ducky2: {}, ", .{isADuck(ducky2)});
    print("ducky3: {}\n", .{isADuck(ducky3)});
}

// This function has a single parameter which is inferred at
// compile time. It uses builtins @TypeOf() and @hasDecl() to
// perform duck typing ("if it walks like a duck and it quacks
// like a duck, then it must be a duck") to determine if the type
// is a "duck".
fn isADuck(possible_duck: anytype) bool {
    // We'll use @hasDecl() to determine if the type has
    // everything needed to be a "duck".
    //
    // In this example, 'has_increment' will be true if type Foo
    // has an increment() method:
    //
    //     const has_increment = @hasDecl(Foo, "increment");
    //
    // Please make sure MyType has both waddle() and quack()
    // methods:
    const MyType = @TypeOf(possible_duck);
    const walks_like_duck = @hasDecl(MyType, "waddle");
    const quacks_like_duck = @hasDecl(MyType, "quack");

    const is_duck = walks_like_duck and quacks_like_duck;

    if (is_duck) {
        // We also call the quack() method here to prove that Zig
        // allows us to perform duck actions on anything
        // sufficiently duck-like.
        //
        // Because all of the checking and inference is performed
        // at compile time, we still have complete type safety:
        // attempting to call the quack() method on a struct that
        // doesn't have it (like Duct) would result in a compile
        // error, not a runtime panic or crash!
        possible_duck.quack();
    }

    return is_duck;
}

inline loop (for comptime type)

inline for (.{u8, u16, u32, u64}) |T| { // these type only available at compile time
    print("{} ", .{@typeInfo(T).int.bits});
}
const foo = [3]*const [5]u8{"~{s}~", "<{s}>", "d{s}b"};
comptime var i = 0;

inline while (i < foo.len) : (i += 1) {
    print(foo[i] ++ "\n", .{foo[i]});
}

comptime expression

// function
// comptime llama();

// expression
// bar = comptime baz();

// block
// comptime {
//   ...
// }

// get a value from a block
// var llama = comptime bar: {
//   ...
// }

sentinel

Zig supports sentinel-terminated arrays, slices, and pointers.

const a: [4:0]u32 = [_]u32{1, 2, 3, 4};
const b: [:0]u32 = &[_]u32{1, 2, 3, 4};
const c: [*:0]u32 = &[_]u32{1, 2, 3, 4};

@ptrCast() to go back to sentinel-terminated pointer.

pub fn main() void {
    // Again, we've coerced the sentinel-terminated string to a
    // many-item pointer, which has no length or sentinel.
    const data: [*]const u8 = "Weird Data!";

    // Please cast 'data' to 'printable':
    const printable: [*:0]const u8 = @ptrCast(data);

    print("{s}\n", .{printable});
}

quoted-identifiers

pub fn main() void {
    const @"55_cows": i32 = 55;
    const @"isn't true": bool = false;

    print("Sweet freedom: {}, {}.\n", .{
        @"55_cows",
        @"isn't true",
    });
}

interface

inline else for interface method.

const Insect = union(enum) {
    ant: Ant,
    bee: Bee,
    grasshopper: Grasshopper,

    // Thanks to 'inline else', we can think of this print() as
    // being an interface method. Any member of this union with
    // a print() method can be treated uniformly by outside
    // code without needing to know any other details. Cool!
    pub fn print(self: Insect) void {
        switch (self) {
            inline else => |case| return case.print(),
        }
    }
};

async

Concurrency backends:

  • Threaded - thread-pool based I/O
  • Evented - chooses the best event-loop backend according to OS:
    • Uring on Linux (iouring)
    • Kqueue on BSD/MacOS
    • Dispatch on MacOS (Grand Central Dispatch)

Io struct:

const Io = struct {
    userdata: ?*anyopaque,      // opqaue state of the backend
    vtable: *const VTable,      // table of function pointers
};

// In Zig 0.16, main() receives a std.process.Init struct to opt into I/O and concurrency support
pub fn main(init: std.process.Init) !void {}

async/await/cancel

var future = io.async(someFunction, .{arg1, arg2}); // NOTE: the future may run immediately or on another thread
defer _ = future.cancel(io);                        // safety
const result = future.await(io);                    // NOTE: every future MUST be either .await or .cancel, leaks resources otherwise.
// both .await and .cancel BLOCK until the task finished and return the result.
// calling either one more than once is safe - subsquent calls just return a copy of the result!

group tasks

When we have many tasks that don't return individual values, use a Group! A Group is an unordered set of tasks that can only be awaited or canceled as a whole:

var group: std.Io.Group = .init;
group.async(io, myTask, .{arg1});
group.async(io, myTask, .{arg2});
try group.await(io);            // blocks until all tasks finish

Important rules:

  1. The return type of functions spawned in a group must be coercible to Cancelble!void (i.e. void, or errorCanceled!void).
  2. Once call group.async(), MUST eventually call group.await() or group.cancel() to release resources.
  3. group.cancel() requests cancellation on ALL members, then blocks until they all finish.

structured cancellation

A cancellation point is any Io function that can return error.Canceled - most commonly io.sleep(). The different from killing a thread is that the task gets a chance to "clean up" and return a value.

fn myTask(io: std.Io) u32 {
    io.sleep(...) catch |err| switch (err) {
        error.Canceled => return 0,
    };
    return 5;
}

race multiple tasks

Select is like a Group, but lets we receive individual results as tasks complete - one at a time:

const Race = std.Io.Select(union(enum) {
    fast: u32,
    slow: u32,
});
var buffer: [2]Race.Union = undefined;
var sel = Race.init(io, &buffer);

sel.async(.fast, fastFn, .{io});
sel.async(.slow, slowFn, .{io});

const winner = try sel.await(); // returns first completed
switch (winner) {
    .fast => |var| ...,
    .slow => |var| ...,
}
sel.cancelDiscard();            // cancel remaining, discard results

synchronization

When multiple async tasks access shared data, we need synchronization.

Mutex (exclusive)
var mutex: std.Io.Mutex = .init;
try mutex.lock(io);             // blocks until lock is acquired, it's a cancellation point which can return error.Canceled.
defer mutex.unlock(io);
// ... critical section for safely modify shared data ...
RwLock
Semaphore
Futex
Batch
Condition

communication

Queue (bounded, thread-safe channel)
var backing: [16]u32 = undefined;
var queue: std.Io.Queue(u32) = .init(&backing);

try queue.putOne(io, value);    // blocks if queue is full

const val = try queue.getOne(io); // blocks if queue is empty

individual dispatch

The difference between io.async and io.concurrency: io.aync():

  • The function MAY run on a separate unit of concurrency or it MAY run immediately on the caller.
  • Never fails - if no concurrency is available, it just runs the function right away.
  • More portable, works with all Io backends.

io.concurrency():

  • Guarantees a separate unit of concurrency.
  • Can fail with error.ConcurrencyUnavailable if resources exhausted or the backend doesn't support it.
  • Use when we need the task to run independently of the caller.

cancel protection

Sometimes a task has a critical section that MUST NOT be interrupted.

const old = io.swapCancelProtection(.blocked);
defer _ = io.swapCancelProtection(old);
// ... do some stuff safely ...  io.checkCancel() return error.Canceled if a cancel request is pending

memory allocation (heap)

var area = std.heap.ArenaAllocator.init(std.heap.page.allocator);
defer arena.deinit();

const allocator = arena.allocator();
const ptr = try allocator.create(i32);

const slice_ptr = try allocator.alloc(f64, 5);

bit manipulation

<<
>>
&
|
^

testing

const testing = std.testing;

test "description" {
    try testing.expect(...);
    try testing.expectEqual(...);
    try testing.expectError(error.xxx, ...);
}

packed

internal is integer!!

const foo = packed struct { ... };
const bar = packed union { ... };

const U = packed union(u2) {
    a: u2,
    b: i2,
};
// we could leverage this same memory region for different purpose
comptime {
    const u: U = .{ .a = 3 };
    switch (u) {
        .{ .a = 3 } => {}       // ok
        .{ .a = 2 },
        .{ .a = 1 },
        .{ .a = 0 },
    }
}

FFI

C

const c = @cImport({
    @cInclude("unistd.h");
});

pub fn main() void {
    const c_res = c.write(2, "Hello C from Zig!", 17);
}

Creator: Emacs 32.0.50 (Org mode 10.0-pre)