TL;DR
Learn Zig arrays and structs — create fixed-size arrays, define structs with methods, and organize data structures for clean, efficient programs.
Key concepts
- zig arrays
- zig structs
- zig data structures
- zig struct methods
- zig arrays tutorial
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:
- Create an array of your favorite numbers and calculate their average
- Define a
Bookstruct with title, author, and pages. Create an array of books - Write a struct method that modifies the struct (returns a new instance)
- Create a
Circlestruct with methods for area and circumference - Build a
Studentstruct that contains an array of test scores and a method to calculate GPA - 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
selfas 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.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.