Zig Slices
A slice is a window into an array. It knows where a portion of memory starts and how many elements it contains, without owning that memory itself. Slices let you write functions that work with any portion of any array — regardless of the array's full size.
The Slice Model
Full array:
Index: [ 0 | 1 | 2 | 3 | 4 | 5 | 6 ]
Value: [10 |20 |30 |40 |50 |60 |70 ]
Slice: arr[2..5]
↑ ↑
start end (exclusive)
Sees: [30 | 40 | 50 ] (indices 2, 3, 4)
A slice internally stores: +------------------+ | pointer → arr[2] | Points to element at index 2 | length = 3 | Covers 3 elements +------------------+
Creating a Slice
const arr = [_]u32{ 10, 20, 30, 40, 50 };
const slc: []const u32 = arr[1..4];
for (slc) |val| {
std.debug.print("{d}\n", .{val});
}
// Output: 20, 30, 40
The range 1..4 starts at index 1 and ends before index 4. Index 4 is not included. The slice slc sees exactly three elements: those at positions 1, 2, and 3 in the original array.
Slice Syntax Variants
arr[2..5] → elements at index 2, 3, 4 arr[2..] → from index 2 to end of array arr[..5] → from start to index 4 (5 exclusive) arr[0..arr.len] → entire array as a slice (same as arr[..])
Mutable Slices
A slice of a var array can modify the original array's elements through the slice:
var data = [_]i32{ 1, 2, 3, 4, 5 };
const mid: []i32 = data[1..4];
mid[0] = 99; // Modifies data[1]
mid[2] = 77; // Modifies data[3]
std.debug.print("{d}\n", .{data});
// Output: { 1, 99, 3, 77, 5 }
data: [ 1 | 2 | 3 | 4 | 5 ]
↑ ↑
mid[0]=99 mid[2]=77
After modification:
data: [ 1 | 99 | 3 | 77 | 5 ]
The slice does not copy the data — it points to the same memory. Changing elements through the slice changes the original array. This is intentional and efficient: no copying happens.
Read-Only Slices
Use []const u32 when you want to read but not modify:
fn printAll(items: []const u32) void {
for (items) |item| {
std.debug.print("{d} ", .{item});
}
std.debug.print("\n", .{});
}
The const in the slice type is a promise: this function will not change what the slice points to. The compiler enforces this — any attempt to write through a []const slice fails at compile time.
String as a Slice
In Zig, strings are slices of bytes — specifically []const u8. A string literal is a slice pointing to read-only memory:
const greeting: []const u8 = "Hello, World!";
std.debug.print("Length: {d}\n", .{greeting.len}); // 13
std.debug.print("First char: {c}\n", .{greeting[0]}); // H
"Hello, World!"
[ H | e | l | l | o | , | | W | o | r | l | d | ! ]
0 1 2 3 4 5 6 7 8 9 10 11 12
greeting.len = 13
greeting[0] = 'H' (72 in ASCII)
Working with strings in Zig is working with byte slices. The standard library provides tools in std.mem for searching, comparing, and splitting string slices.
Slice of a Slice
You can slice a slice to get a narrower view:
const sentence: []const u8 = "The quick brown fox";
const word: []const u8 = sentence[4..9];
std.debug.print("{s}\n", .{word}); // quick
"The quick brown fox"
0123456789...
sentence[4..9]:
index 4 = 'q'
index 8 = 'k' (last included)
→ "quick"
Sentinel-Terminated Slices
C strings end with a zero byte. Zig represents these with sentinel-terminated slices:
Type: [:0]const u8 Meaning: Slice of bytes where a zero byte follows the last element. const c_str: [:0]const u8 = "hello"; // Memory: ['h','e','l','l','o', 0] // ↑ sentinel (zero byte)
This type is essential when passing strings to C libraries that expect null-terminated strings. Zig's type system tracks whether a slice is sentinel-terminated, preventing you from accidentally passing a plain slice where a C string is required.
Slice Bounds Checking
const arr = [_]u32{ 1, 2, 3 };
const bad = arr[0..10]; // arr has only 3 elements!
// → Compile error (if size known at compile time)
// or Runtime panic (if size only known at runtime)
Zig checks slice bounds in debug builds. Attempting to create a slice that goes beyond the array's end causes an immediate panic with a clear message. Release builds skip these checks for performance — your responsibility is to verify bounds are correct before building in release mode.
Common Standard Library Slice Functions
std.mem.eql(u8, a, b) → compare two slices for equality std.mem.startsWith(u8, s, p) → does s start with prefix p? std.mem.endsWith(u8, s, s2) → does s end with suffix s2? std.mem.indexOf(u8, hay, ndl) → find needle in haystack slice std.mem.copy(u8, dest, src) → copy bytes from src to dest
Practical Example: Finding a Word in Text
const std = @import("std");
pub fn main() void {
const text: []const u8 = "Zig is fast and safe";
const word: []const u8 = "fast";
if (std.mem.indexOf(u8, text, word)) |pos| {
std.debug.print("Found '{s}' at position {d}\n", .{word, pos});
} else {
std.debug.print("'{s}' not found.\n", .{word});
}
}
Output:
Found 'fast' at position 11
std.mem.indexOf returns an optional usize — the position if found, or null if not. The if-capture pattern if (...) |pos| unwraps the position only when it exists. If the word is absent, the else branch runs. This single pattern replaces the need for special sentinel return values like -1 used in C.
