ArrayList
An ArrayList is one of the most important data structures in Zig.
It is a growable array.
A normal Zig array has a fixed size:
const numbers = [_]i32{ 1, 2, 3 };This array always contains exactly 3 items. You cannot add a fourth item later.
But many real programs need collections that grow and shrink dynamically:
- reading lines from a file
- storing network packets
- collecting search results
- building strings
- parsing JSON
- tracking game objects
That is where ArrayList becomes useful.
An ArrayList automatically manages a dynamic buffer in memory. When the list becomes full, it allocates a larger buffer and moves the data.
You can think of it like this:
Fixed Array:
[1][2][3]
ArrayList:
capacity = 8
length = 3
[1][2][3][ ][ ][ ][ ][ ]The list currently stores 3 items, but it already reserved space for 8.
This allows fast appends.
Importing ArrayList
ArrayList lives inside Zig’s standard library.
const std = @import("std");The actual type is:
std.ArrayList(T)T is the element type.
Example:
std.ArrayList(i32)This means:
“An ArrayList storing
i32values.”
Creating an ArrayList
Here is the smallest working example:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
}Read this slowly.
Step 1: Create an Allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};An ArrayList uses heap memory internally.
That means it needs an allocator.
The general purpose allocator is Zig’s standard heap allocator for normal programs.
Step 2: Get the Allocator Interface
const allocator = gpa.allocator();This gives us a std.mem.Allocator value.
The allocator is passed into containers like ArrayList.
Step 3: Create the List
var list = std.ArrayList(i32).init(allocator);This creates an empty list of i32.
At this point:
length = 0
capacity = 0No elements exist yet.
Step 4: Free the Memory
defer list.deinit();ArrayList may allocate heap memory.
You must release that memory later.
deinit() frees the internal buffer.
This is extremely important in Zig. Memory cleanup is explicit.
Appending Elements
Now let’s add items.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var list = std.ArrayList(i32).init(allocator);
defer list.deinit();
try list.append(10);
try list.append(20);
try list.append(30);
std.debug.print("{any}\n", .{list.items});
}Output:
{ 10, 20, 30 }Why append Uses try
Notice this:
try list.append(10);Appending may require allocating more memory.
Memory allocation can fail.
So append returns an error union.
That is why we use try.
This is one of Zig’s major ideas:
operations that can fail must say so explicitly
Understanding items
The actual elements live here:
list.itemsitems is a slice.
Type:
[]i32You can use it like a normal slice.
Example:
std.debug.print("{}\n", .{list.items[0]});Output:
10You can loop over it:
for (list.items) |value| {
std.debug.print("{}\n", .{value});
}Length vs Capacity
An ArrayList tracks two important numbers.
Length
How many elements currently exist.
[1][2][3]
^
length = 3Capacity
How many elements fit before reallocating.
[1][2][3][ ][ ][ ][ ][ ]
^
capacity = 8You can inspect them:
std.debug.print("len = {}\n", .{list.items.len});
std.debug.print("capacity = {}\n", .{list.capacity});Automatic Growth
When the list becomes full, Zig allocates a larger buffer.
Example:
capacity = 4
[1][2][3][4]Appending one more item might create:
capacity = 8
[1][2][3][4][5][ ][ ][ ]The old data gets copied into the new buffer.
This is why appending is usually fast, but occasionally more expensive.
Removing Elements
pop()
Removes the last item.
const value = list.pop();Example:
try list.append(10);
try list.append(20);
const x = list.pop();
std.debug.print("{}\n", .{x});Output:
20The list now contains only:
10Inserting Elements
You can insert into the middle.
try list.insert(1, 99);Example:
Before:
[10][20][30]
After:
[10][99][20][30]Elements shift to the right.
Removing by Index
_ = list.orderedRemove(1);Example:
Before:
[10][20][30]
After:
[10][30]This preserves order.
But shifting elements costs time.
Fast Unordered Removal
If order does not matter:
_ = list.swapRemove(1);Example:
Before:
[10][20][30][40]Remove index 1:
After:
[10][40][30]The last element moves into the removed slot.
This is much faster.
Reserving Capacity
Sometimes you already know the approximate size.
Example:
try list.ensureTotalCapacity(1000);This preallocates space.
Advantages:
- fewer reallocations
- fewer memory copies
- better performance
Very important for performance-sensitive code.
Clearing the List
list.clearRetainingCapacity();This removes all items but keeps the allocated memory.
Useful when reusing buffers repeatedly.
Example:
Before:
len = 1000
capacity = 2048
After clear:
len = 0
capacity = 2048No new allocation needed later.
Converting to Owned Slice
Sometimes you want the final buffer itself.
const slice = try list.toOwnedSlice();After this:
- the caller owns the memory
- the list becomes empty
- the caller must free the slice later
This is common in parsers and builders.
ArrayList of Strings
You can store more than integers.
Example:
var list = std.ArrayList([]const u8).init(allocator);Now each item is a string slice.
Example:
try list.append("apple");
try list.append("banana");Loop:
for (list.items) |item| {
std.debug.print("{s}\n", .{item});
}Output:
apple
bananaCommon Beginner Mistake: Forgetting deinit
Wrong:
var list = std.ArrayList(i32).init(allocator);If you never call:
list.deinit();you leak memory.
Always pair:
init()with:
deinit()Usually with defer.
Common Beginner Mistake: Keeping Old Pointers
This is dangerous:
const ptr = &list.items[0];
try list.append(999);Appending may reallocate memory.
If reallocation happens, ptr may point to invalid memory.
This is a very important concept.
Pointers into an ArrayList can become invalid after resizing.
Internal Mental Model
A simplified internal structure looks like this:
const ArrayList = struct {
ptr: [*]T,
len: usize,
capacity: usize,
};Not exact implementation, but close enough conceptually.
The list owns a heap buffer:
ptr ---> [10][20][30][ ]len tracks used items.
capacity tracks allocated space.
Why ArrayList Matters
ArrayList appears everywhere in Zig programs.
It is used for:
- dynamic strings
- parsers
- token lists
- file buffers
- HTTP requests
- JSON processing
- compiler internals
- game entities
- network packets
If you understand ArrayList, you understand one of the core patterns of Zig programming:
- explicit memory ownership
- allocator-driven design
- slices as views into memory
- manual lifetime management
- predictable performance
This is not just a container.
It teaches the philosophy of Zig itself.