Skip to editor content
learningzig.orglesson 5 of 25

Arrays and Structs

In this lesson, we'll explore Zig's data structures. You'll learn how to work with arrays, slices, and structs to organize and manage your data effectively.

What You'll Learn

  • How to create and use arrays in Zig
  • The difference between arrays and slices
  • How to define and use structs
  • Methods and namespaced functions
  • Working with nested data structures
  • Common patterns for data organization

Arrays

Arrays in Zig have a fixed size known at compile time:

const std = @import("std");

pub fn main() !void {
    // Array with explicit size and type
    const numbers: [5]i32 = [5]i32{1, 2, 3, 4, 5};

    // Array with inferred size (use [_])
    const fruits = [_][]const u8{"apple", "banana", "cherry"};

    // Access elements
    std.debug.print("First number: {}\n", .{numbers[0]});
    std.debug.print("Second fruit: {s}\n", .{fruits[1]});

    // Array length
    std.debug.print("Array length: {}\n", .{numbers.len});

    // Initialize array with same value
    const zeros = [_]i32{0} ** 10; // Array of 10 zeros
    std.debug.print("Zeros length: {}\n", .{zeros.len});
}

Array Operations

Work with arrays using loops and common patterns:

const std = @import("std");

pub fn main() !void {
    var numbers = [_]i32{10, 20, 30, 40, 50};

    // Modify array elements
    numbers[0] = 15;
    std.debug.print("Modified first element: {}\n", .{numbers[0]});

    // Iterate over array
    for (numbers) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n", .{});

    // Iterate with index
    for (numbers, 0..) |num, i| {
        std.debug.print("[{}]: {} ", .{i, num});
    }
    std.debug.print("\n", .{});

    // Sum array elements
    var sum: i32 = 0;
    for (numbers) |num| {
        sum += num;
    }
    std.debug.print("Sum: {}\n", .{sum});
}

Slices

Slices are views into arrays with runtime-known length:

const std = @import("std");

pub fn main() !void {
    const numbers = [_]i32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Create a slice (pointer + length)
    const slice1 = numbers[0..5]; // Elements 0-4
    const slice2 = numbers[5..]; // Elements 5 to end
    const slice3 = numbers[0..]; // Entire array as slice

    std.debug.print("First slice: ", .{});
    for (slice1) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n", .{});

    std.debug.print("Second slice: ", .{});
    for (slice2) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n", .{});

    // Full-range slice covers every element
    std.debug.print("Full slice: ", .{});
    for (slice3) |num| {
        std.debug.print("{} ", .{num});
    }
    std.debug.print("\n", .{});

    // Slice length
    std.debug.print("Slice1 length: {}, full slice length: {}\n", .{ slice1.len, slice3.len });

    // String is a slice of bytes
    const message: []const u8 = "Hello, Zig!";
    std.debug.print("Message: {s}\n", .{message});
    std.debug.print("Message length: {}\n", .{message.len});

    // Try creating your own slices!
}

Structs

Structs group related data together:

const std = @import("std");

// Define a struct
const Person = struct {
    name: []const u8,
    age: u32,
    height: f32,

    // Method (function inside struct)
    pub fn introduce(self: Person) void {
        std.debug.print("Hi, I'm {s}, {} years old, {d:.2}m tall\n",
            .{self.name, self.age, self.height});
    }

    // Associated function (no self parameter)
    pub fn create(name: []const u8, age: u32, height: f32) Person {
        return Person{
            .name = name,
            .age = age,
            .height = height,
        };
    }
};

pub fn main() !void {
    // Create struct instance
    const person1 = Person{
        .name = "Alice",
        .age = 30,
        .height = 1.75,
    };

    // Access fields
    std.debug.print("Name: {s}\n", .{person1.name});
    std.debug.print("Age: {}\n", .{person1.age});

    // Call method
    person1.introduce();

    // Use associated function
    const person2 = Person.create("Bob", 25, 1.80);
    person2.introduce();
}

Struct Patterns

Common patterns for working with structs:

const std = @import("std");

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

    // Calculate area
    pub fn area(self: Rectangle) f32 {
        return self.width * self.height;
    }

    // Calculate perimeter
    pub fn perimeter(self: Rectangle) f32 {
        return 2 * (self.width + self.height);
    }

    // Check if square
    pub fn isSquare(self: Rectangle) bool {
        return self.width == self.height;
    }

    // Resize
    pub fn scale(self: Rectangle, factor: f32) Rectangle {
        return Rectangle{
            .width = self.width * factor,
            .height = self.height * factor,
        };
    }
};

pub fn main() !void {
    const rect = Rectangle{
        .width = 10.0,
        .height = 5.0,
    };

    std.debug.print("Area: {d:.2}\n", .{rect.area()});
    std.debug.print("Perimeter: {d:.2}\n", .{rect.perimeter()});
    std.debug.print("Is square: {}\n", .{rect.isSquare()});

    const scaled = rect.scale(2.0);
    std.debug.print("Scaled width: {d:.2}\n", .{scaled.width});
}

Nested Structs

Structs can contain other structs:

const std = @import("std");

const Address = struct {
    street: []const u8,
    city: []const u8,
    zip: []const u8,
};

const Employee = struct {
    name: []const u8,
    id: u32,
    address: Address,

    pub fn displayInfo(self: Employee) void {
        std.debug.print("Employee: {s} (ID: {})\n", .{self.name, self.id});
        std.debug.print("Address: {s}, {s} {s}\n",
            .{self.address.street, self.address.city, self.address.zip});
    }
};

pub fn main() !void {
    const employee = Employee{
        .name = "Alice Smith",
        .id = 12345,
        .address = Address{
            .street = "123 Main St",
            .city = "Springfield",
            .zip = "12345",
        },
    };

    employee.displayInfo();
}

Arrays of Structs

Combine arrays and structs for collections of data:

const std = @import("std");

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

    pub fn distance(self: Point, other: Point) f32 {
        const dx = self.x - other.x;
        const dy = self.y - other.y;
        return @sqrt(dx * dx + dy * dy);
    }

    pub fn display(self: Point) void {
        std.debug.print("({d:.1}, {d:.1})", .{self.x, self.y});
    }
};

pub fn main() !void {
    // Array of points
    const points = [_]Point{
        Point{ .x = 0.0, .y = 0.0 },
        Point{ .x = 3.0, .y = 4.0 },
        Point{ .x = 1.0, .y = 1.0 },
    };

    // Display all points
    for (points, 0..) |point, i| {
        std.debug.print("Point {}: ", .{i});
        point.display();
        std.debug.print("\n", .{});
    }

    // Calculate distances
    const origin = points[0];
    std.debug.print("\nDistances from origin:\n", .{});
    for (points[1..], 1..) |point, i| {
        const dist = origin.distance(point);
        std.debug.print("Point {}: {d:.2}\n", .{i, dist});
    }

    // Try adding more points or methods!
}

Packed Structs

Packed structs have guaranteed memory layout:

const std = @import("std");

// Packed struct - guaranteed memory layout
const Flags = packed struct {
    is_active: bool,
    is_admin: bool,
    is_verified: bool,
    _padding: u5 = 0, // Pad to 8 bits
};

pub fn main() !void {
    const flags = Flags{
        .is_active = true,
        .is_admin = false,
        .is_verified = true,
    };

    std.debug.print("Active: {}\n", .{flags.is_active});
    std.debug.print("Admin: {}\n", .{flags.is_admin});
    std.debug.print("Verified: {}\n", .{flags.is_verified});
    std.debug.print("Size: {} bytes\n", .{@sizeOf(Flags)});
}

Practice Exercises

Try these exercises:

  1. Create an array of your favorite numbers and calculate their average
  2. Define a Book struct with title, author, and pages. Create an array of books
  3. Write a struct method that modifies the struct (returns a new instance)
  4. Create a Circle struct with methods for area and circumference
  5. Build a Student struct that contains an array of test scores and a method to calculate GPA
  6. Create a slice from an array and modify elements through the slice

Challenge: Create a TodoList struct that contains an array of Task structs (each with description and completed status). Add methods to mark tasks complete and count remaining tasks.

Key Takeaways

  • Arrays have compile-time known size; slices have runtime-known size
  • Use [_] for array size inference and [start..end] for slicing
  • Structs group related data and can contain methods
  • Methods take self as first parameter; associated functions don't
  • Structs can be nested and stored in arrays
  • Packed structs provide guaranteed memory layout for low-level programming

Next Steps

Now that you can organize data with arrays and structs, a natural question arises: what happens when something goes wrong? Functions fail, files go missing, and input arrives in unexpected shapes. Zig handles all of this through its error union system, which bakes failure information directly into return types so that errors are impossible to ignore.

Next lesson

Error Handling

Master Zig error handling — use error unions, error sets, try/catch, and errdefer to write robust code that fails gracefully without exceptions.

22 min