A struct in Zig is a compound type that groups related fields under a single name. So far, we have worked with simple types or collections of the same type, but the real world is not that uniform.
A “User” has a name (text), an age (integer), and a status (boolean). We need a way to package these heterogeneous data into a single logical unit.
In Zig, this tool is the struct.
If you come from C++, Java, or C#, you might think of structs as classes. Although both group data and behavior, Zig does not incorporate inheritance or implicit dynamic dispatch.
Declaration and Initialization
A struct in Zig is declared by assigning the definition to a constant. This is important: the type is a value that is known at compile time.
const Player = struct {
name: []const u8,
points: u32,
active: bool,
};To create an instance of this Player, we use a very explicit syntax where we name each field.
const p1 = Player{
.name = "Luis",
.points = 1500,
.active = true,
};Notice the dot . before each field (.name). Zig uses this to clearly distinguish field assignment from other operations. Furthermore, the initialization order does not matter, but you must initialize all fields.
Methods: Functions Inside Structs
Zig does not have “methods” in the strict sense that they belong to an object. It simply allows writing functions within the namespace of the struct.
To define operations on the value, we conventionally use the parameter self.
Initialization Functions
If a function does not take the struct as a parameter, it is a static function. By convention, we often create one called init to act as a constructor.
const Player = struct {
points: u32,
// "Static" function (does not receive self)
pub fn init(initial_points: u32) Player {
return Player{
.points = initial_points,
};
}
};
// Usage:
const p = Player.init(100);Instance Methods
If the first parameter of the function is the struct itself (or a pointer to it), we can call it using the dot syntax object.function().
const Player = struct {
points: u32,
// Receives a mutable pointer to itself
pub fn gainPoints(self: *Player, amount: u32) void {
self.points += amount;
}
// Receives a read-only pointer
pub fn print(self: *const Player) void {
std.debug.print("Points: {d}\n", .{self.points});
}
};
pub fn main() void {
var p = Player{ .points = 100 };
// Syntactic Sugar: Zig converts this to Player.gainPoints(&p, 50)
p.gainPoints(50);
p.print();
}Note: self is not a special reserved keyword (like this in Java). It’s just the name we use by convention for the first argument. You could call it this or context, but use self so everyone understands you.
Structs and Classes in Memory
In Java, objects include metadata managed by the runtime. In C++, a class with virtual functions usually incorporates a pointer to its virtual table.
A normal struct in Zig does not add an object header or a virtual table by itself. It may contain alignment padding between fields.
This guarantees that:
- No implicit dispatch: any polymorphism mechanism must be expressed in the code.
- Direct representation: the value contains its fields and the necessary padding to align them.
- Contiguous data: arrays of structs store their elements next to each other.
Field Reordering
A technical curiosity: Zig reserves the right to reorder the fields of your struct in memory to optimize alignment and use less RAM (padding).
If you need a representation compatible with the C ABI, use extern struct. For bit-level defined fields, there is packed struct; they are not interchangeable, and byte order still depends on architecture or external format.
Default Values
We can assign default values to struct fields. This allows us to omit them during initialization.
const Configuration = struct {
port: u16 = 8080,
host: []const u8 = "localhost",
debug: bool = false,
};
// We can initialize empty and it takes the defaults
const config = Configuration{};
// Or override only what we want
const prod_config = Configuration{ .debug = true };No Inheritance, There Is Composition
Zig does not have class inheritance. You cannot do struct Dog extends Animal.
Zig’s philosophy (and that of many modern languages like Go or Rust) is: Prefer composition over inheritance.
If you want to reuse code, simply put a struct inside another.
const Engine = struct {
horsepower: u32,
};
const Car = struct {
brand: []const u8,
// Composition: The car HAS AN engine
engine: Engine,
};This makes dependencies explicit. There are no complex hierarchies where a change in the “Grandparent” class breaks the “Grandchild” code.
Anonymous Structs
Sometimes we need to group data quickly for a single use (for example, in the .{} of debug.print). Zig allows nameless literal structs.
// .{} is an empty anonymous struct
std.debug.print("Hello", .{});
// Anonymous struct with fields
const point = .{ .x = 10, .y = 20 };