Skip to content

Struct Methods

A struct method is a function that belongs to a struct.

A struct method is a function that belongs to a struct.

Start with a normal struct:

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

Now add a function inside it:

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

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

The function print is inside Point, so you call it through Point or through a Point value.

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

p.print();

This prints:

(10, 20)

The important part is this parameter:

self: Point

By convention, Zig uses the name self for the value the method works on.

When you write:

p.print();

Zig treats it like:

Point.print(p);

So method syntax is mostly convenient function-call syntax.

Methods Are Just Functions

Zig does not have classes. A method is still a function.

This:

p.print();

is shorthand for this:

Point.print(p);

That means Zig keeps the model simple. A struct contains fields and declarations. A function inside a struct is one of those declarations.

There is no hidden this. There is no automatic inheritance. There is no object system behind the scenes.

You decide what gets passed.

Methods That Read Data

A method that only reads a struct can take self by value:

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

    fn sum(self: Point) i32 {
        return self.x + self.y;
    }
};

Use it like this:

const p = Point{
    .x = 3,
    .y = 4,
};

const result = p.sum();

Now result is 7.

Passing by value copies the struct. For small structs, this is fine.

For larger structs, use a pointer:

fn sum(self: *const Point) i32 {
    return self.x + self.y;
}

Now the method receives a pointer to the original value, but it promises not to modify it.

The *const Point type means “pointer to a Point that I will not change.”

Methods That Change Data

A method that changes the struct should take a mutable pointer:

const Counter = struct {
    value: i32,

    fn increment(self: *Counter) void {
        self.value += 1;
    }
};

Use it like this:

var counter = Counter{
    .value = 0,
};

counter.increment();
counter.increment();

After this, counter.value is 2.

The method uses:

self: *Counter

That means it receives a mutable pointer to the original Counter.

This matters. Without a pointer, the method would modify only a copy.

Value Receiver vs Pointer Receiver

There are three common forms:

fn method(self: Type) void

Use this when the struct is small and the method only needs a copy.

fn method(self: *const Type) void

Use this when the method only reads the value and you want to avoid copying.

fn method(self: *Type) void

Use this when the method needs to modify the value.

Example:

const Buffer = struct {
    len: usize,

    fn length(self: Buffer) usize {
        return self.len;
    }

    fn lengthNoCopy(self: *const Buffer) usize {
        return self.len;
    }

    fn clear(self: *Buffer) void {
        self.len = 0;
    }
};

The receiver type tells the reader what the method can do.

Constructor-Like Functions

Zig does not have constructors.

Instead, you usually write a normal function that returns a struct value.

A common name is init:

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

    fn init(id: u64, name: []const u8) User {
        return User{
            .id = id,
            .name = name,
        };
    }
};

Use it like this:

const user = User.init(1, "Ada");

This is clear. User.init is just a function that returns a User.

There is no hidden allocation. There is no automatic setup step. The function does exactly what the code says.

Methods Can Return New Values

A method does not always modify the current value. It can return a new value instead.

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

    fn moved(self: Point, dx: i32, dy: i32) Point {
        return Point{
            .x = self.x + dx,
            .y = self.y + dy,
        };
    }
};

Use it like this:

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

const p2 = p1.moved(5, -3);

Now:

p1 is still (10, 20)
p2 is (15, 17)

This style is useful when you want immutable values.

Methods Can Call Other Methods

Methods can call other methods in the same struct:

const Rectangle = struct {
    width: u32,
    height: u32,

    fn area(self: Rectangle) u32 {
        return self.width * self.height;
    }

    fn isSquare(self: Rectangle) bool {
        return self.width == self.height;
    }

    fn describe(self: Rectangle) void {
        std.debug.print("area = {}, square = {}\n", .{
            self.area(),
            self.isSquare(),
        });
    }
};

Here, describe calls area and isSquare.

const r = Rectangle{
    .width = 10,
    .height = 20,
};

r.describe();

This keeps related behavior near the data.

Public and Private Methods

Struct functions can be public or private.

A private function uses fn:

fn helper() void {
    // private
}

A public function uses pub fn:

pub fn init() Type {
    // public
}

Inside one file, this difference may not seem important. It matters when another file imports your code.

Example:

const Counter = struct {
    value: i32,

    pub fn init() Counter {
        return Counter{ .value = 0 };
    }

    pub fn increment(self: *Counter) void {
        self.value += 1;
    }

    fn secretHelper(self: Counter) i32 {
        return self.value * 2;
    }
};

Code outside this file can call Counter.init and counter.increment.

It cannot call secretHelper.

A Complete Example

const std = @import("std");

const Counter = struct {
    value: i32,

    pub fn init(start: i32) Counter {
        return Counter{
            .value = start,
        };
    }

    pub fn increment(self: *Counter) void {
        self.value += 1;
    }

    pub fn add(self: *Counter, amount: i32) void {
        self.value += amount;
    }

    pub fn get(self: Counter) i32 {
        return self.value;
    }
};

pub fn main() void {
    var counter = Counter.init(10);

    counter.increment();
    counter.add(5);

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

Output:

counter = 16

Read the flow:

var counter = Counter.init(10);

Create a counter with value 10.

counter.increment();

Change it to 11.

counter.add(5);

Change it to 16.

counter.get()

Read the final value.

Why Methods Matter

Methods keep operations close to the data they operate on.

Instead of writing unrelated functions everywhere, you can group them with the struct:

const Counter = struct {
    value: i32,

    pub fn increment(self: *Counter) void {
        self.value += 1;
    }
};

This makes the program easier to navigate. When you find the Counter type, you also find the important operations for Counter.

The main rule is simple: put a function inside a struct when the function strongly belongs to that type.