Skip to content

Struct Definitions

A struct is a type that groups several values together.

A struct is a type that groups several values together.

You use a struct when one idea has several pieces of data. For example, a point has an x value and a y value. A user has an id, a name, and an email. A file entry has a path, a size, and a timestamp.

In Zig, a struct is written like this:

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

This defines a new type named Point.

The fields are:

x: i32
y: i32

Each field has a name and a type.

Now we can create a Point value:

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

The field names start with a dot:

.x = 10
.y = 20

This style is explicit. When you read the code, you know exactly which value goes into which field.

Accessing Fields

You access struct fields with dot syntax:

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("x = {}, y = {}\n", .{ p.x, p.y });
}

Output:

x = 10, y = 20

The expression p.x means “the x field of p.”

The expression p.y means “the y field of p.”

Structs Are Types

A struct definition creates a type.

This line:

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

means: create an anonymous struct type, then store that type in the constant Point.

That may sound strange at first, but it fits Zig’s model. Types are values that exist at compile time.

So Point is not a variable containing a point. It is the type itself.

This creates a value of that type:

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

You can think of it like this:

Point is the shape.
p is one actual value with that shape.

Mutable Struct Values

If you declare a struct value with var, you can change its fields:

const std = @import("std");

const Counter = struct {
    value: i32,
};

pub fn main() void {
    var c = Counter{
        .value = 0,
    };

    c.value += 1;
    c.value += 1;

    std.debug.print("counter = {}\n", .{c.value});
}

Output:

counter = 2

Because c is declared with var, its field can be changed.

If c were declared with const, this would fail:

const c = Counter{
    .value = 0,
};

c.value += 1; // error

A const struct value cannot be modified.

Field Order

Fields are usually written one per line:

const User = struct {
    id: u64,
    name: []const u8,
    email: []const u8,
};

When creating a struct value, you should normally use named fields:

const user = User{
    .id = 1,
    .name = "Ada",
    .email = "ada@example.com",
};

This is safer than relying on position. The code stays clear even when the struct has many fields.

Zig expects you to initialize all fields unless a field has a default value.

Default Field Values

A struct field can have a default value:

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

Now you can create a value without explicitly setting every field:

const config = Config{};

This uses the defaults:

port = 8080
debug = false

You can override one field and keep the other default:

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

Now debug is true, but port is still 8080.

Default values are useful for configuration structs and test data.

Empty Structs

A struct can have no fields:

const Empty = struct {};

This may look useless, but empty structs are sometimes useful for marker types, namespaces, and compile-time patterns.

For now, just know that it is legal.

Structs as Function Parameters

You can pass structs to functions:

const std = @import("std");

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

fn printPoint(p: Point) void {
    std.debug.print("({}, {})\n", .{ p.x, p.y });
}

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

    printPoint(p);
}

Output:

(3, 4)

The function parameter says:

p: Point

That means p must be a Point.

Returning Structs from Functions

A function can also return a struct:

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

fn origin() Point {
    return Point{
        .x = 0,
        .y = 0,
    };
}

This function returns a Point value.

You can use it like this:

const p = origin();

Structs are ordinary values. They can be stored, passed, returned, copied, and placed inside other structs.

Nested Structs

A struct can contain another struct:

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

const Rectangle = struct {
    top_left: Point,
    bottom_right: Point,
};

Now a rectangle is made of two points:

const rect = Rectangle{
    .top_left = Point{
        .x = 0,
        .y = 0,
    },
    .bottom_right = Point{
        .x = 100,
        .y = 50,
    },
};

You can access nested fields with repeated dot syntax:

const x = rect.top_left.x;

This means:

take rect
then take its top_left field
then take that point's x field

Anonymous Struct Values

Zig can create anonymous struct values:

const person = .{
    .name = "Ada",
    .age = 36,
};

Here, we did not write a named type like Person. Zig infers an anonymous struct type from the fields.

This is useful in small local cases, especially for formatting arguments:

std.debug.print("{} {}\n", .{ "hello", 123 });

The expression .{ "hello", 123 } is an anonymous tuple-like struct.

For main data models, prefer named structs:

const Person = struct {
    name: []const u8,
    age: u8,
};

Named structs make your program easier to read.

Structs Can Contain Functions

A Zig struct can contain declarations, including functions:

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

    fn zero() Point {
        return Point{
            .x = 0,
            .y = 0,
        };
    }
};

Now zero belongs to the Point namespace:

const p = Point.zero();

This does not mean Zig has classes in the same way as Java or C++. A struct is still a data type. But a struct can also act as a namespace for functions related to that data.

We will study this more in the next section on struct methods.

Structs Are Often Used as Namespaces

Sometimes a struct is used only to group declarations:

const Math = struct {
    fn add(a: i32, b: i32) i32 {
        return a + b;
    }

    fn sub(a: i32, b: i32) i32 {
        return a - b;
    }
};

You can call these functions like this:

const x = Math.add(10, 5);
const y = Math.sub(10, 5);

Here, Math has no fields. It is used as a namespace.

This is common in Zig because Zig does not have a separate class or namespace keyword. Structs cover that role.

Struct Literals

A struct literal is the syntax used to create a struct value:

Point{
    .x = 10,
    .y = 20,
}

The general form is:

TypeName{
    .field_name = value,
    .field_name = value,
}

Example:

const Book = struct {
    title: []const u8,
    pages: u32,
};

const book = Book{
    .title = "The Zig Book",
    .pages = 300,
};

The type name comes first. The field assignments go inside braces.

Structs and Memory Layout

A normal Zig struct does not promise that fields are placed in memory exactly in the order you wrote them.

The compiler may choose a layout that is efficient.

For most beginner code, this does not matter. You should access fields by name, not by memory position.

When exact memory layout matters, Zig has special forms such as extern struct and packed struct.

For example, an extern struct is useful when matching a C struct layout:

const CPoint = extern struct {
    x: c_int,
    y: c_int,
};

A packed struct is useful when controlling bit-level layout:

const Flags = packed struct {
    read: bool,
    write: bool,
    execute: bool,
};

Do not use these forms by default. Use normal structs first. Reach for layout-specific structs only when you really need them.

A Practical Example

Here is a small example using a struct to represent a bank account:

const std = @import("std");

const Account = struct {
    id: u64,
    balance: i64,
};

fn deposit(account: *Account, amount: i64) void {
    account.balance += amount;
}

pub fn main() void {
    var account = Account{
        .id = 1001,
        .balance = 500,
    };

    deposit(&account, 250);

    std.debug.print("account {} balance = {}\n", .{
        account.id,
        account.balance,
    });
}

Output:

account 1001 balance = 750

This line is important:

fn deposit(account: *Account, amount: i64) void

The parameter account: *Account means the function receives a pointer to an Account.

That allows the function to modify the original account.

This call passes a pointer:

deposit(&account, 250);

The &account expression means “the address of account.”

Without a pointer, the function would receive a copy.

Copying Struct Values

Struct values are copied when assigned or passed by value.

Example:

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

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

var b = a;
b.x = 99;

After this:

a.x is still 1
b.x is 99

The assignment var b = a; copies the value.

This is simple and predictable. But if a struct is large, copying may be expensive. In that case, you may pass a pointer instead.

When to Use a Struct

Use a struct when several values belong together.

Good examples:

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

const User = struct {
    id: u64,
    name: []const u8,
};

const FileInfo = struct {
    path: []const u8,
    size: u64,
};

A struct gives a name to a concept in your program. Instead of passing separate loose values everywhere, you group them into one meaningful value.

Compare this:

fn draw(x: i32, y: i32, width: i32, height: i32) void {
    // ...
}

With this:

const Rect = struct {
    x: i32,
    y: i32,
    width: i32,
    height: i32,
};

fn draw(rect: Rect) void {
    // ...
}

The second version is easier to understand because the data has a name.

The Main Idea

A struct is Zig’s basic tool for building your own data types.

It lets you take separate values and combine them into one named concept.

const Person = struct {
    name: []const u8,
    age: u8,
};

This says: a Person has a name and an age.

That is the essence of structs. They make data organized, explicit, and easier to pass around.