Getting Started with Zig for Developers Experienced with Other Languages

The content is based on the official Zig documentation.

Hello, World

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
}

!void tells the Zig compiler that the function will either return an error or a value. It is an error union, the full form is <error set type>!<any data type>, combining an Error Set Type (An error set is like an enum) and any other data type.

!void , the error set type is not explicitly written on the left side of the ! operator.

The try expression evaluates the result of stdout.print. If the result is an error, then the try expression will return from main with the error. Otherwise, the program will continue.

// top-level declarations are order-independent
const print = std.debug.print;
const std = @import("std");

// std.debug.print cannot fail, so the return type is void, not !void
pub fn main() void {
    // must have ".{}", std.debug.print needs two arguments
    print("Hello, world\n", .{});
}

Zig Test

// testing_introduction.zig

const std = @import("std");
const expect = std.testing.expect;

test "expect addOne adds one to 41" {
    try expect(addOne(41) == 42);
}

// A test name can also be written using an identifier
test addOne {
    try expect(addOne(41) == 42);
}

fn addOne(number: i32) i32 {
    return number + 1;
}
# run the code directly
zig test testing_introduction.zig

Test declarations can be written in the same file, where code under test is written, or in a separate Zig source file.

Since test declarations are top-level declarations, they are order-independent and can be written before or after the code under test.

We can easily run our testing code like this, no need to compile it first.

Comments

No multiline comments in Zig.

Zig supports 3 types of comments:

  • Normal comments.

    Begins with //.

  • Doc comments

    Begins with exactly three slashes (i.e.///).

  • Top-level doc comments

    Begins with //!. It documents the current module.

The last two are used by the compiler to generate the package documentation.

No /*...*/.

Integers

// underscores may be placed as a visual separator
const one_billion = 1_000_000_000;

Zig supports arbitrary bit-width integers, the identifier i7 refers to a signed 7-bit integer.

Floats

There is no syntax for NaN, infinity, or negative infinity. For these special values, one must use the standard library:

const std = @import("std");

const inf = std.math.inf(f32);
const negative_inf = -std.math.inf(f64);
const nan = std.math.nan(f128);

String

Zig has no concept of strings. String literals are const pointers to null-terminated arrays of u8.

The encoding of a string in Zig is de-facto assumed to be UTF-8.

It is possible to embed non-UTF-8 bytes into a string literal using \xNN notation.

// Multiline string literals
const hello_world_in_c =
    \\#include <stdio.h>
    \\
    \\int main(int argc, char **argv) {
    \\    printf("hello world\n");
    \\    return 0;
    \\}
;

Optionals

const std = @import("std");
const expect = std.testing.expect;

test "using optionals" {
    // ?u32: u32 or null
    const o: ?u32 = 10;
    // o.?: access the value
    try expect(o.? == 10);
}

Assignment

const x = 1234;
var y: i32 = 5678;
var z: i32 = undefined;

If you need a variable that you can modify, use the var keyword, otherwise, const.

undefined means the value could be anything. undefined means “Not a meaningful
value. Using this value would be a bug. The value will be unused, or overwritten before being used.”

Operators

  • a orelse b

    If a is null, returns b (as the default value), otherwise returns the unwrapped value of a.

  • a.?

    a orelse unreachable.

    unreachable emits a call to panic with the message reached unreachable code.

  • a catch b, a catch |err| b

    If a is an error, returns b (as the default value), otherwise returns the unwrapped value of a.

    The latter captures the error, err is a variable to hold the error value and can be used in the expression b.

  • a ++ b

    Array concatenation.

  • a ** b

    Array multiplication.

    // ababab
    "ab" ** 3;

Arrays

// array literal
const message = [_]u8{ 'h', 'e', 'l', 'l', 'o' };

// A string literal is a single-item pointer to an array.
const same_message = "hello";

// modifiable array
var some_integers: [100]i32 = undefined;

// initialize an array to zero
const all_zero = [_]u16{0} ** 10;

// call a function to initialize an array
const Point = struct {
    x: i32,
    y: i32,
};

var more_points = [_]Point{makePoint(3)} ** 10;

fn makePoint(x: i32) Point {
    return Point{
        .x = x,
        .y = x * 2,
    };
}

// Multidimensional Arrays
const mat4x4 = [4][4]f32{
    [_]f32{ 1.0, 0.0, 0.0, 0.0 },
    [_]f32{ 0.0, 1.0, 0.0, 1.0 },
    [_]f32{ 0.0, 0.0, 1.0, 0.0 },
    [_]f32{ 0.0, 0.0, 0.0, 1.0 },
};

// The syntax [N:x]T describes Sentinel-Terminated arrays
// We can use the sentinel element to determine the end
// arr[4] == 0, the sentinel element
// arr.len == 4, not 5
const arr = [_:0]u8 {1, 2, 3, 4};

Slices

A slice is a pointer and a length.

The difference between an array and a slice is that the array’s length is part of the type and known at compile-time, whereas the slice’s length is known at runtime. Both can be accessed with the len field.

const hello: []const u8 = "hello";

var array = [_]i32{ 1, 2, 3, 4 };
var known_at_runtime_zero: usize = 0;
const slice = array[known_at_runtime_zero..array.len];

// If you slice with comptime-known start and end positions,
// the result is a pointer to an array, rather than a slice.
const array_ptr = array[0..array.len];

// A comptime-known length. A runtime-known start position.
// Slicing twice allows the compiler to perform some optimisations.
var runtime_start: usize = 1;
const length = 2;
const array_ptr_len = array[runtime_start..][0..length];

// The syntax [:x]T describes Sentinel-Terminated slices
// It has a runtime-known length
// slice[5] == 0, the sentinel element
// slice.len == 5, not 6
const slice: [:0]const u8 = "hello";

// Another Sentinel-Terminated slice syntax: data[start..end :x]
// "data" is a many-item pointer, array or slice.
// array[3] must be the same value as the sentinel value: 0,
// otherwise, runtime panic
var array = [_]u8{ 3, 2, 1, 0, 3, 2, 1, 0 };
var runtime_length: usize = 3;
const slice = array[0..runtime_length :0];

Function parameters that are “strings” are expected to be UTF-8 encoded slices of u8.

Vectors

A vector is a group of booleans, Integers, Floats, or Pointers which are operated on in parallel, using SIMD instructions if possible.

Vector types are created with the builtin function @Vector.

Vectors support the same builtin operators as their underlying base types. These operations return a vector of the same length as the input vectors.

// Vectors have a compile-time known length and base type.
const a = @Vector(4, i32){ 1, 2, 3, 4 };

// Vectors and fixed-length arrays can be automatically
// assigned back and forth
var arr1: [4]f32 = [_]f32{ 1.1, 3.2, 4.5, 5.6 };
var vec: @Vector(4, f32) = arr1;
var arr2: [4]f32 = vec;

// You can also assign from a slice with comptime-known
// length to a vector using .*
const vec2: @Vector(2, f32) = arr1[1..3].*;

// vec2 == vec3
// &arr1 is a pointer
var slice: []const f32 = &arr1;
var offset: u32 = 1;
const vec3: @Vector(2, f32) = slice[offset..][0..2].*;

Pointers

Two kinds of pointers: single-item and many-item

  • *T

    single-item pointer to exactly one item.

  • [*]T

    many-item pointer to unknown number of items.

// single-item pointer
var x: i32 = 1234;
const x_ptr = &x;

// x_ptr.*: dereference a pointer
// assigning the value to '_', we don't use the return value
_ = x_ptr.* == 1234;
x_ptr.* += 1;

// many-item pointer
const array = [_]i32{ 1, 2, 3, 4 };
const ptr: [*]const i32 = &array;
const expect = @import("std").testing.expect;

test "optional pointers" {
    // Pointers cannot be null. If you want a null pointer,
    // use the optional.
    // prefix `?` to make the pointer type optional.
    var ptr: ?*i32 = null;

    var x: i32 = 1;
    ptr = &x;

    // dereference a optional pointer
    try expect(ptr.?.* == 1);
    // error: comparison of '*i32' with null
    // try expect(ptr.? != null);
    // error: comparison of 'i32' with null
    // try expect(ptr.?.* != null);
    try expect(ptr != null);
}
// An array of [*]const u8, its length is 1
const window_name = [1][*]const u8{"window name"};
// An array of ?[*]const u8
const x: [*]const ?[*]const u8 = &window_name;

Zig supports pointer arithmetic. It’s better to assign the pointer to [*]T and increment that variable.

var array = [_]i32{ 1, 2, 3, 4 };
var length: usize = 0;
var slice = array[length..array.len];
// don't do this
// now the slice is in an bad state since len has not been updated
// slice.len is still 4
slice.ptr += 1;
// Conversion between an integer address, a pointer
const ptr: *i32 = @ptrFromInt(0xdeadbee0);
const addr = @intFromPtr(ptr);

// pointer casting
const bytes align(@alignOf(u32)) = [_]u8{ 0x12, 0x12, 0x12, 0x12 };
// @ptrCast is an unsafe operation
// u32_ptr.* == 0x12121212
const u32_ptr: *const u32 = @ptrCast(&bytes);

// a straightforward way to do the above than pointer casting
// u32_value == 0x12121212
const u32_value = @as(u32, @bitCast(bytes))

// another way, using a slice narrowing cast
// u32_value == 0x12121212
const u32_value = std.mem.bytesAsSlice(u32, bytes[0..])[0];
// Sentinel-Terminated Pointers
pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;

pub fn main() !void {
    // ok
    _ = printf("Hello, world!\n");
    // error: expected type '[*:0]const u8', found '[14:0]u8'
    const msg = "Hello, world!\n";
    _ = printf(msg.*);
}

The syntax [*:x]T describes a pointer that has a length determined by a sentinel value. This provides protection against buffer overflow and overreads.

Slices have bounds checking, generally prefer Slices rather than Sentinel-Terminated Pointers.

struct

Zig gives no guarantees about the order of fields and the size of the struct.

const Point = struct {
    x: f32 = 2.0, // default value
    y: f32,
};

// an instance
var p = Point {
    .x = 0.12,
    .y = undefined,
};

// calculate a struct base pointer given a field pointer
const point = @fieldParentPtr(Point, "x", &p.x);

// Anonymous struct literals
// no copy
var pt: Point = .{
    .x = 0.12,
    .y = 3.0,
};

// Structs can have methods
const Vec = struct {
    x: f32,
    y: f32,
    z: f32,

    pub fn init(x: f32, y: f32, z: f32) Vec {
        return Vec {
            .x = x,
            .y = y,
            .z = z,
        };
    }

    pub fn dot(self: Vec, other: Vec) f32 {
        return self.x * other.x + self.y * other.y + self.z * other.z;
    }
};

// Structs can have declarations
// Structs can have 0 fields
// @sizeOf(Empty) == 0
// Empty.PI is a struct namespaced variable
const Empty = struct {
    pub const PI = 3.14;
};
// you can still instantiate an empty struct
const does_nothing = Empty {};

// Return a struct from a function
// This is how we do generics in Zig
fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            prev: ?*Node,
            next: ?*Node,
            data: T,
        };

        first: ?*Node,
        last:  ?*Node,
        len:   usize,
    };
}
var list = LinkedList(i32) {
    .first = null,
    .last = null,
    .len = 0,
};

const ListOfInts = LinkedList(i32);
var node = ListOfInts.Node {
    .prev = null,
    .next = null,
    .data = 1234,
};
var list2 = LinkedList(i32) {
    .first = &node,
    .last = &node,
    .len = 1,
};
// When using a pointer to a struct, fields can be accessed directly,
// without explicitly dereferencing the pointer.
// equal: list2.first.?.*.data
_ = list2.first.?.data;

Other types of struct:

// Anonymous structs can be created without specifying field names,
// and are referred to as "tuples". 
// The fields are implicitly named using numbers starting from 0.
const values = .{
    @as(u32, 1234),
    @as(f64, 12.34),
    true,
    "hi",
} ++ .{false} ** 2;
// values.len == 6
// values.@"3"[0] == 'h'

Like arrays, tuples have a .len field, can be indexed (provided the index is comptime-known) and work with the ++ and ** operators.

enum

const Type = enum {
    ok,
    not_ok,
};
// @typeInfo can be used to access the integer tag type of an enum.
// @typeInfo(Type).Enum.tag_type == u2

const c = Type.ok;
// Enum literals
// Specifying the name of an enum field without specifying the enum type
const c2: Color = .ok;

// If you want access to the ordinal value of an enum, you
// can specify the tag type.
const Value = enum(u2) {
    zero,
    one,
    two,
};
// Now you can cast between u2 and Value.
// @intFromEnum(Value.zero) == 0

// You can override the ordinal value for an enum.
const Value2 = enum(u32) {
    hundred = 100,
    thousand = 1000,
    million = 1000000,
};
// @intFromEnum(Value2.hundred) == 100

// You can also override only some values.
const Value3 = enum(u4) {
    a,
    b = 8,
    c,
    d = 4,
    e,
};
// @intFromEnum(Value3.a) == 0
// @intFromEnum(Value3.c) == 9
// @intFromEnum(Value3.e) == 5

// Enums can have methods, the same as structs and unions.
// Enum methods are not special, they are only namespaced
// functions
const Suit = enum {
    clubs,
    spades,
    diamonds,
    hearts,

    pub fn isClubs(self: Suit) bool {
        return self == Suit.clubs;
    }
};

// Non-exhaustive enum
// Have a trailing '_' field
const Number = enum(u8) {
    one,
    two,
    three,
    _,
};
// A switch on a non-exhaustive enum can include a '_' prong
// as an alternative to an else prong
const result = switch (number) {
    .one => true,
    .two,
    .three => false,
    _ => false,
};

There is extern enum, to be compatible with the C ABI.

union

Only one field can be active at a time. The in-memory representation of bare unions is not guaranteed.

Use @ptrCast, or use an extern union or a packed union which have guaranteed in-memory layout.

const Payload = union {
    int: i64,
    float: f64,
    boolean: bool,
};
// "int" field is active
var payload = Payload{ .int = 1234 };
// to access the value
_ = payload.int;
// To activate another field by assigning the entire union
payload = Payload{ .float = 12.34 };

// anonymous union literal
var p: Payload = .{.int = 42};

// Tagged union, used with switch expressions
const ComplexTypeTag = enum {
    ok,
    not_ok,
};
// The field names of ComplexType and ComplexTypeTag must be same 
const ComplexType = union(ComplexTypeTag) {
    ok: u8,
    not_ok: void,
};
var c = ComplexType{ .ok = 42 };

switch (c) {
    // |x| syntax captures the matched value
    // *value make it a pointer
    ComplexTypeTag.ok => |*value| value.* += 1,
    ComplexTypeTag.not_ok => unreachable,
}
// c.ok == 43

// Tagged union, but to infer enum tag type
// Pay attention to the differences 'union(ComplexTypeTag)'
// and 'union(enum)'
const Variant = union(enum) {
    int: i32,
    boolean: bool,
    // void can be omitted when inferring enum tag type.
    none, // void

    // unions can have methods just like structs and enums
    fn truthy(self: Variant) bool {
        return switch (self) {
            Variant.int => |x_int| x_int != 0,
            Variant.boolean => |x_bool| x_bool,
            Variant.none => false,
        };
    }
};

Other types of union:

opaque

opaque {} declares a new type with an unknown (but non-zero) size and alignment.

It can contain declarations the same as structs, unions, and enums.

This is typically used for type safety when interacting with C code that does not expose struct details.

const Derp = opaque {};
const Wat = opaque {};

extern fn bar(d: *Derp) void;
fn foo(w: *Wat) callconv(.C) void {
    // error: expected type '*test_opaque.Derp', found
    // '*test_opaque.Wat'
    bar(w);
}

test "call foo" {
    foo(undefined);
}

Blocks

Blocks are expressions.

Blocks are used to limit the scope of variable declarations.

{
    var x: i32 = 1;
    _ = x;
}

// An empty block is equivalent to void{}
const a = {};
const b = void{};

// labeled block
// break can be used to return a value from the block
// "blk" can be any name
const x = blk: {
    y += 1;
    break :blk y;
};

Shadowing

const pi = 3.14;
{
    // error: local variable 'pi' shadows local constant from outer
    // scope
    var pi: i32 = 1234;
}

// This is ok
{
    const pi = 3.14;
    _ = pi;
}
{
    var pi: bool = true;
    _ = pi;
}

Variables

Identifiers must start with an alphabetic character or underscore and may be followed by any number of alphanumeric characters or underscores. They must not overlap with any keywords.

const @"identifier with spaces in it" = 0xff;

const c = @import("std").c;
pub extern "c" fn @"error"() void;

const Color = enum {
  red,
  @"really red",
};
const color: Color = .@"really red";

If a name that does not fit these requirements is needed, the @"" syntax may be used.

var y: i32 = add(10, x);
const x: i32 = add(12, 34);

const S = struct {
    var x: i32 = 1234;
};

Container level variables have static lifetime and are order-independent and lazily analyzed.

The initialization value of container level variables is implicitly comptime. If a container level variable is const then its value is comptime-known, otherwise it is runtime-known.

A container in Zig is any syntactical construct that acts as a namespace to hold variable and function declarations.

Containers are also type definitions which can be instantiated. Structs, enums, unions, opaques, and even Zig source files themselves are containers.

Although containers (except Zig source files) use curly braces to surround their definition, they should not be confused with blocks or functions. Containers do not contain statements.

fn foo() i32 {
    const S = struct {
        var x: i32 = 1234;
    };
    S.x += 1;
    return S.x;
}

It is also possible to have local variables with static lifetime by using containers inside functions.

// ...
test "comptime vars" {
    comptime var y: i32 = 1;
    y += 1;

    try expect(y == 2);
}

A local variable may be qualified with the comptime keyword. This causes the variable’s value to be comptime-known, and all loads and stores of the variable to happen during semantic analysis of the program, rather than at runtime.
All variables declared in a comptime expression are implicitly comptime variables.

switch

const a: u64 = 10;
const zz: u64 = 103;

const b = switch (a) {
    // Multiple cases
    1, 2, 3 => 0,

    // Ranges. These are inclusive of both ends.
    5...100 => 1,

    // if a == 101, b == 11
    101 => blk: {
        const c: u64 = 5;
        break :blk c * 2 + 1;
    },

    // Switching on arbitrary expressions is allowed as long as the
    // expression is known at compile-time.
    // if a == 103, b == 103
    zz => zz,

    // if a == 105, b == 107
    blk: {
        const d: u32 = 5;
        const e: u32 = 100;
        break :blk d + e;
    } => 107,

    // When a switch expression does not have an else clause,
    // it must exhaustively list all the possible values.
    else => 9,
};

// Switching with Enum literals
const Color = enum {
    auto,
    off,
    on,
};
const color = Color.off;
const result = switch (color) {
    .auto => false,
    .on => false,
    .off => true,
};

// switch can be used to capture the field values of a Tagged union
const Point = struct {
    x: u8,
    y: u8,
};
// if 'Item' is not a tagged union,
// error: union 'Item' has no member named 'a'
// error: union field missing type. The error message is about 'Item.d'
const Item = union(enum) {
    a: u32,
    c: Point,
    d, // void
    e: u32,
};

var a = Item{ .c = Point{ .x = 1, .y = 2 } };

const b = switch (a) {
    // |item| captures the matched value
    Item.a, Item.e => |item| item,

    // *item make it a pointer
    Item.c => |*item| blk: {
        item.*.x += 1;
        break :blk 6;
    },

    Item.d => 8,
};

// Inline switch
fn isFieldOptional(comptime T: type, field_index: usize) !bool {
    const fields = @typeInfo(T).Struct.fields;
    return switch (field_index) {
        // can't write the code like this
        // the error message:
        // error: values of type '[]const builtin.Type.StructField'
        // must be comptime-known, but index value is runtime-known

        // 0...fields.len - 1 =>  @typeInfo(fields[field_index].type)
        //    == .Optional,

        // This prong is analyzed `fields.len - 1` times with `idx`
        // being a unique comptime-known value each time.
        inline 0...fields.len - 1 => |idx| @typeInfo(fields[idx].type)
            == .Optional,
        else => return error.IndexOutOfBounds,
    };
}

const Struct1 = struct { a: u32, b: ?u32 };

isFieldOptional(Struct1, 0); // false
isFieldOptional(Struct1, 1); // true

// Inline switch, to obtain the union's enum tag value
const U = union(enum) {
    a: u32,
    b: f32,
};
fn getNum(u: U) u32 {
    switch (u) {
        // 'inline else' let the compiler to check every possible case
        // 'tag' is to obtain the union's enum tag value
        inline else => |num, tag| {
            if (tag == .b) {
                return @intFromFloat(num);
            }
            return num;
        }
    }
}
var u = U{ .b = 42 };
_ = getNum(u) == 42; // true

while

while (true) {
    i += 1;
    if (i < 10)
        continue;
    break;
}

// with a continue expression
while (i < 10) : (i += 1) {}

// with a more complicated continue expression
while (i * j < 2000) : ({ i *= 2; j *= 3; }) {
    // ...
}

// break a loop and return a value
fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
    var i = begin;

    return while (i < end) : (i += 1) {
        if (i == number) {
            // break, like return, accepts a value parameter
            break true;
        }
    // When you break from a while loop, the else branch is not
    // evaluated. otherwise, it will be executed when the condition
    // of the while loop is tested as false.
    } else false;
}

// Labeled while, to use 'break'
outer: while (true) {
    while (true) {
        break :outer;
    }
}

// Labeled while, to use 'continue'
outer: while (i < 10) : (i += 1) {
    while (true) {
        continue :outer;
    }
}

// With optionals
var numbers_left: u32 = 3;
// ?u32, an optional: u32 or null
fn eventuallyNullSequence() ?u32 {
    // Zig style "if...else"
    return if (numbers_left == 0) null else blk: {
        numbers_left -= 1;
        break :blk numbers_left;
    };
}
var sum: u32 = 0;
// When the |x| syntax is present, the while condition must be
// an optional.
// The loop exits when null is encountered.
// |value| captures the return value of eventuallyNullSequence()
while (eventuallyNullSequence()) |value| {
    sum += value;
} else {
    // will be here on the first null value encountered
    // sum == 3
    //...
}
// while with Error Unions

const expect = @import("std").testing.expect;

test "while error union capture" {
    var sum1: u32 = 0;
    numbers_left = 3;
    // the condition is an error union
    while (eventuallyErrorSequence()) |value| {
        sum1 += value;
    // capturing the error, the loop is finished
    // "else |x|" syntax is present, the while condition must have
    // an Error Union Type
    } else |err| {
        try expect(err == error.ReachedZero);
    }
}

var numbers_left: u32 = undefined;

// !u32, not ?u32, they are different
// !u32 is an error union
fn eventuallyErrorSequence() !u32 {
    return if (numbers_left == 0) error.ReachedZero else blk: {
        numbers_left -= 1;
        break :blk numbers_left;
    };
}

inline while causes the loop to be unrolled, which allows the code to do some things which only work at compile time.

It is recommended to use inline loops only for one of these reasons:

  • To execute at comptime for the semantics to work.

  • You have a benchmark to prove that forcibly unrolling the loop in this way is measurably faster.

for

// items can also be slice
const items = [_]i32 { 4, 5, 3, 4, 0 };
const items2 = [_]i32 { 1, 2, 3, 4, 5 };

for (items) |value| {
    // ...
}

// items[3] is not included
for (items[0..3]) |value| {
    // ...
}

// "i" is the index
for (items, 0..) |v, i| {
    // ...
}

// To iterate over consecutive integers.
// 3 is not included
for (0..3) |i| {
    // ...
}

// Iterate over multiple objects.
// The length of items and items2 must be equal at the start of the loop
for (items, items2) |v1, v2| {
    // ...
}

// "value" is a pointer
for (&items) |*value| {
    // ...
}

// For loops can also be used as expressions
const result = for (items) |value| {
    // ...
} else {
    // ...
}

// Labeled for
var count: usize = 0;
outer: for (1..6) |_| {
    for (1..6) |_| {
        count += 1;
        break :outer;
        // continue :outer;
    }
}

inline for is like inline while.

if

// If expressions have three types:
// bool
// ?T
// anyerror!T

// To be used as expressions.
const result = if (a != b) 47 else 3089;

// if boolean
const a: u32 = 5;
const b: u32 = 4;
if (a != b) {
    // ...
} else if (a == 9) {
    unreachable;
} else {
    unreachable;
}

// if optional
var c: ?u32 = 3;
if (c) |value| {
    // ...
}
// Access the value by reference using a pointer capture.
if (c) |*value| {
    value.* = 2;
}

// if error union
const a: anyerror!u32 = 1;
if (a) |value| {
    // ...
} else |err| {
    // The else and |err| capture is strictly required.
    _ = err;
    unreachable;
}

if (a) |value| {
    // ...
} else |_| {}

if (a) |*value| {
    value.* = 9;
} else |_| {
    unreachable;
}

// if error union with optional
const a: anyerror!?u32 = 0;
if (a) |optional_value| {
    // optional_value.? == 0
} else |_| {
    unreachable;
}

// a pointer capture
if (a) |*optional_value| {
    if (optional_value.*) |*value| {
        value.* = 9;
    }
} else |_| {
    unreachable;
}

// combining if and switch
if (parseU64(str, 10)) |number| {
    // ...
} else |err| switch (err) {
    // ...
}

defer

const std = @import("std");
const print = std.debug.print;

fn deferErrorExample(is_error: bool) !void {
    print("\nstart of function\n", .{});

    // This will always be executed on exit
    defer {
        print("end of function\n", .{});
    }

    // This will only be executed if the scope returns with an error
    errdefer {
        print("encountered an error!\n", .{});
    }

    if (is_error) {
        return error.DeferError;
    }
}

fn deferErrorCaptureExample() !void {
    // get additional error message
    errdefer |err| {
        print("the error is {s}\n", .{@errorName(err)});
    }

    return error.DeferError;
}

test "errdefer unwinding" {
    deferErrorExample(false) catch {};
    deferErrorExample(true) catch {};
    deferErrorCaptureExample() catch {};
}

If multiple defer statements are specified, they will be executed in the reverse order they were run.

Inside a defer expression the return statement is not allowed.

Functions

// The export specifier makes a function externally visible in the
// generated object file, and makes it use the C ABI.
export fn sub(a: i8, b: i8) i8 { return a - b; }

// The extern specifier is used to declare a function that will be
// resolved at link time
// "c" specifies the library that has the function. ("c" -> libc.so)
extern "c" fn atan2(a: f64, b: f64) f64;

// The inline calling convention forces a function to be inlined at all
// call sites.
// If the function cannot be inlined, it is a compile-time error.
fn shiftLeftOne(a: u32) callconv(.Inline) u32 {
    return a << 1;
}

// The pub specifier allows the function to be visible when importing.
// Another file can use @import and call sub2
pub fn sub2(a: i8, b: i8) i8 { return a - b; }

// Function pointers are prefixed with `*const `.
const Call2Op = *const fn (a: i8, b: i8) i8;
fn doOp(fnCall: Call2Op, op1: i8, op2: i8) i8 {
    return fnCall(op1, op2);
}

Primitive types such as Integers and Floats passed as parameters are copied.

When Structs, unions, and arrays are passed as parameters, Zig may choose to copy and pass by value, or pass by reference, whichever way Zig decides will be faster.

For extern functions, Zig follows the C ABI for passing structs and unions by value.

//  "anytype" will be inferred when the function is called.
fn addFortyTwo(x: anytype) @TypeOf(x) {
    return x + 42;
}

Errors

// "number" is the returned value of parseU64(), or the default 
// value: 13
const number = parseU64(str, 10) catch 13;
// using named block
const number = parseU64(str, 10) catch blk: {
    // do things
    break :blk 13;
};
// if the expression will never be an error
const number = parseU64("1234", 10) catch unreachable;

// The following two are equal
// return an error or continue
const number = parseU64(str, 10) catch |err| return err;
const number = try parseU64(str, 10);

Casting

var a: u8 = 1;
// type coercion - @as builtin
var b = @as(u16, a);

// error: ambiguous coercion of division operands 'comptime_float'
// and 'comptime_int'; non-zero remainder '4'
var f: f32 = 54.0 / 5;
// OK
var f: f32 = 54.0 / @as(f32, 5);

var x: []const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
var x: anyerror![]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };
var x: ?[]const u8 = &[5]u8{ 'h', 'e', 'l', 'l', 111 };

var buf: [5]u8 = "hello".*;
// the array length becomes the slice length
const x: []u8 = &buf;
// Single-item pointers to arrays can be coerced to many-item pointers.
const x2: [*]u8 = &buf;
const x3: ?[*]u8 = &buf;

// Single-item pointers can be cast to len-1 single-item arrays.
var x: i32 = 1234;
const y: *[1]i32 = &x;
const z: [*]i32 = y;

// optionals wrapped in error union
const x: anyerror!?i32 = 1234;
const y: anyerror!?i32 = null;
try expect((try x).? == 1234);
try expect((try y) == null);

comptime

  • Compile-time Parameters

  • Compile-time Variables

  • Compile-time Expressions

// Compile-time Parameters

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
fn gimmeTheBiggerFloat(a: f32, b: f32) f32 {
    return max(f32, a, b);
}
fn gimmeTheBiggerInteger(a: u64, b: u64) u64 {
    return max(u64, a, b);
}

In Zig, types are first-class citizens. They can be assigned to variables, passed as parameters to functions, and returned from functions. However, they can only be used in expressions which are known at compile-time.

// Compile-time Variables

const expect = @import("std").testing.expect;

const CmdFn = struct {
    name: []const u8,
    func: fn(i32) i32,
};

const cmd_fns = [_]CmdFn{
    CmdFn {.name = "one", .func = one},
    CmdFn {.name = "two", .func = two},
    CmdFn {.name = "three", .func = three},
};
fn one(value: i32) i32 { return value + 1; }
fn two(value: i32) i32 { return value + 2; }
fn three(value: i32) i32 { return value + 3; }

fn performFn(comptime prefix_char: u8, start_value: i32) i32 {
    var result: i32 = start_value;
    comptime var i = 0;
    // Because of inline while, the function performFn is generated
    // three different times, for the different values of prefix_char
    // provided
    inline while (i < cmd_fns.len) : (i += 1) {
        if (cmd_fns[i].name[0] == prefix_char) {
            result = cmd_fns[i].func(result);
        }
    }
    return result;
}

test "perform fn" {
    try expect(performFn('t', 1) == 6);
    try expect(performFn('o', 0) == 1);
    try expect(performFn('w', 99) == 99);
}

In Zig, the programmer can label variables as comptime. This guarantees to the compiler that every load and store of the variable is performed at compile-time.

const first_25_primes = firstNPrimes(25);
const sum_of_first_25_primes = sum(&first_25_primes);

fn firstNPrimes(comptime n: usize) [n]i32 {
    var prime_list: [n]i32 = undefined;
    // ...
}

fn sum(numbers: []const i32) i32 {
    // ...
}

test "variable values" {
    try @import("std").testing.expect(sum_of_first_25_primes == 1060);
}

At container level (outside of any function), all expressions are implicitly comptime expressions.

This means that we can use functions to initialize complex static data.

Memory

Memory allocators:

  • std.heap.c_allocator

    Use it when you link to libc.

  • std.heap.FixedBufferAllocator or std.heap.ThreadSafeFixedBufferAllocator

    Use it when you know the maximum number of bytes at compile-time.

  • std.heap.ArenaAllocator

    Use it most of the time.

    const std = @import("std");
    
    pub fn main() !void {
        var arena = std.heap.ArenaAllocator.
                        init(std.heap.page_allocator);
        defer arena.deinit();
    
        const allocator = arena.allocator();
    
        const ptr = try allocator.create(i32);
        std.debug.print("ptr={*}\n", .{ptr});
    }
  • std.testing.FailingAllocator

    Use it when writing a test and making sure error.OutOfMemory is handled correctly.

  • std.testing.allocator

    Use it when writing a test.

  • std.heap.GeneralPurposeAllocator

    If none of the above apply, you need a general purpose allocator.

var declarations inside functions are stored in the function’s stack frame. Once a function returns, any Pointers to variables in the function’s stack frame become invalid references

var declarations at the top level or in struct declarations are stored in the global data section.

The location of memory allocated with allocator.alloc or allocator.create is determined by the allocator’s implementation.