Skip to content

Zero Values and Initialization

Initialization means giving a value to something when it is created.

Initialization means giving a value to something when it is created.

const x: i32 = 10;

Here, x is initialized with 10.

A value should usually be initialized immediately. This keeps the program simple. When a reader sees the name, they also see the value it starts with.

Zig Does Not Automatically Zero Everything

In some languages, variables may automatically start as zero, false, or empty.

Zig does not want you to depend on hidden initialization.

This is clear:

var count: usize = 0;

This is also clear:

const enabled: bool = false;

The starting value is visible in the code.

Zig prefers this style because systems programs need precise control. Sometimes you want zero. Sometimes you want null. Sometimes you want an empty array. Sometimes you want uninitialized memory for performance. These are different choices, so Zig makes you write the choice.

Zero Is a Real Value

Zero is not the same as undefined.

var x: i32 = 0;

This creates an integer with a meaningful value: zero.

var x: i32 = undefined;

This creates storage for an integer, but the value is not meaningful yet.

Use zero when zero is the correct starting value.

var total: i32 = 0;
var index: usize = 0;
var bytes_read: usize = 0;

These are good uses of zero. A sum often starts at zero. An index often starts at zero. A byte counter often starts at zero.

Boolean Initialization

A boolean should be initialized to either true or false.

var found: bool = false;

This is common when searching:

const std = @import("std");

pub fn main() void {
    const numbers = [_]i32{ 10, 20, 30 };

    var found: bool = false;

    for (numbers) |n| {
        if (n == 20) {
            found = true;
            break;
        }
    }

    std.debug.print("found = {}\n", .{found});
}

Output:

found = true

The initial value false means “we have not found it yet.”

Numeric Initialization

Numeric variables often start with 0.

var sum: i32 = 0;
var count: usize = 0;

Example:

const std = @import("std");

pub fn main() void {
    const numbers = [_]i32{ 3, 4, 5 };

    var sum: i32 = 0;

    for (numbers) |n| {
        sum += n;
    }

    std.debug.print("sum = {}\n", .{sum});
}

Output:

sum = 12

The variable sum must have a real starting value because each loop step reads the old value before writing the new one.

This line:

sum += n;

means:

sum = sum + n;

So sum must already contain a valid number.

Arrays Filled with Zero

You can create an array filled with zeros.

var buffer = [_]u8{0} ** 64;

This creates an array of 64 bytes, each set to 0.

The syntax:

[_]u8{0} ** 64

means “repeat this one-element array 64 times.”

A smaller example:

const numbers = [_]u8{0} ** 4;

This creates:

0, 0, 0, 0

This is useful when you need predictable initial contents.

Arrays with Specific Values

You can also initialize each element manually.

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

Zig infers the length. This array has type:

[3]i32

You can write the length explicitly too:

const numbers: [3]i32 = .{ 10, 20, 30 };

Both forms are common.

Struct Initialization

A struct value should be initialized by naming its fields.

const Point = struct {
    x: i32,
    y: i32,
};

const p = Point{
    .x = 10,
    .y = 20,
};

This creates a Point with x = 10 and y = 20.

Complete example:

const std = @import("std");

const Point = struct {
    x: i32,
    y: i32,
};

pub fn main() void {
    const p = Point{
        .x = 10,
        .y = 20,
    };

    std.debug.print("({}, {})\n", .{ p.x, p.y });
}

Output:

(10, 20)

Field names make the code clear. You can see exactly which value belongs to which field.

Default Field Values

Struct fields can have default values.

const Config = struct {
    port: u16 = 8080,
    debug: bool = false,
};

Now you can create a value using the defaults:

const config = Config{};

This means:

const config = Config{
    .port = 8080,
    .debug = false,
};

You can also override one field:

const config = Config{
    .debug = true,
};

Then port uses its default value, and debug is set to true.

Complete example:

const std = @import("std");

const Config = struct {
    port: u16 = 8080,
    debug: bool = false,
};

pub fn main() void {
    const config = Config{
        .debug = true,
    };

    std.debug.print("port = {}, debug = {}\n", .{
        config.port,
        config.debug,
    });
}

Output:

port = 8080, debug = true

Defaults are useful for configuration structs, options, and values with common settings.

Optional Initialization

An optional value can contain either a value or null.

var maybe_number: ?i32 = null;

This means there is no integer right now.

Later, you can store an integer:

maybe_number = 42;

This is different from zero.

var a: ?i32 = null;
var b: ?i32 = 0;

a contains no integer.

b contains an integer, and that integer is zero.

That distinction is important. Zero is a value. null means no value.

Sentinel Values

Sometimes programs use a special value to mean “not found.”

For example:

const not_found: i32 = -1;

But in Zig, an optional is often clearer:

fn findNumber() ?usize {
    return null;
}

This says directly that the function may return an index, or may return no index.

Prefer optionals when “no value” is part of the meaning.

Initializing with a Function Call

A value can be initialized from a function result.

fn defaultPort() u16 {
    return 8080;
}

const port = defaultPort();

This is still initialization. The value is known after the function call returns.

Complete example:

const std = @import("std");

fn square(x: i32) i32 {
    return x * x;
}

pub fn main() void {
    const result = square(7);

    std.debug.print("result = {}\n", .{result});
}

Output:

result = 49

Initialization Before Use

The safest rule is simple: initialize before use.

This is correct:

var count: usize = 0;
count += 1;

This is wrong:

var count: usize = undefined;
count += 1;

Why? Because count += 1 reads the old value of count. But the old value is undefined.

This would be valid:

var count: usize = undefined;
count = 0;
count += 1;

But it is worse than the simpler version:

var count: usize = 0;
count += 1;

Use the simple version.

Initialization and const

A const must be initialized when it is declared.

const x: i32 = 10;

You cannot declare a constant and assign it later.

const x: i32; // invalid
x = 10;

This makes sense. A constant gets one value, and that value cannot change.

Initialization and var

A var also usually has an initializer.

var count: usize = 0;

When you need delayed initialization, you can use undefined with an explicit type:

var value: i32 = undefined;
value = 42;

But for beginner code, avoid this unless there is a clear reason.

A Complete Example

const std = @import("std");

const Stats = struct {
    count: usize = 0,
    sum: i32 = 0,
};

pub fn main() void {
    const numbers = [_]i32{ 4, 8, 15, 16, 23, 42 };

    var stats = Stats{};

    for (numbers) |n| {
        stats.count += 1;
        stats.sum += n;
    }

    std.debug.print("count = {}, sum = {}\n", .{
        stats.count,
        stats.sum,
    });
}

Output:

count = 6, sum = 108

The struct has default values:

const Stats = struct {
    count: usize = 0,
    sum: i32 = 0,
};

So this:

var stats = Stats{};

starts with:

count = 0
sum = 0

That makes the loop safe, because both fields have real values before they are updated.

The Main Idea

Initialization is the act of giving storage a real value.

Use 0 when zero is meaningful. Use false when a boolean starts false. Use null when an optional has no value. Use default struct fields when a type has common starting values. Use undefined only when you will write a real value before reading it.

Clear initialization makes Zig code easier to read and harder to misuse.