A string literal is text written directly in your source code.
const name = "Zig";The text between the double quotes is the string literal:
"Zig"In Zig, strings are bytes. More precisely, a string literal is a constant sequence of UTF-8 bytes.
That means "Zig" contains these three visible bytes:
Z i gTheir byte values are:
90 105 103Zig does not have a special built-in String type like many high-level languages. Most Zig code represents text as:
[]const u8Read this as:
read-only slice of bytesThat is the most common type for text passed to functions.
A Basic String Literal
Here is a complete program:
const std = @import("std");
pub fn main() void {
const message = "Hello, Zig!";
std.debug.print("{s}\n", .{message});
}Output:
Hello, Zig!The {s} formatter tells Zig to print the bytes as a string.
This line:
const message = "Hello, Zig!";creates a string literal and gives it the name message.
You can pass it to a function that expects []const u8:
fn printMessage(message: []const u8) void {
std.debug.print("{s}\n", .{message});
}Usage:
printMessage("Hello");This works because a string literal can be used as a read-only byte slice.
Strings Are Read-Only
String literals are constant data.
This is valid:
const name = "Zig";This is not valid:
name[0] = 'B';You cannot modify a string literal.
The reason is simple: string literals are stored as read-only program data. Many parts of the program may refer to the same literal. Allowing mutation would be unsafe and confusing.
Use a mutable array if you want text that can change:
var name = [_]u8{ 'Z', 'i', 'g' };
name[0] = 'B';Now the bytes contain:
B i gThis is not a string literal anymore. It is a mutable array of bytes.
String Literals and []const u8
Most functions that read text should accept this type:
[]const u8Example:
const std = @import("std");
fn greet(name: []const u8) void {
std.debug.print("Hello, {s}!\n", .{name});
}
pub fn main() void {
greet("Zig");
greet("Ada");
greet("C");
}Output:
Hello, Zig!
Hello, Ada!
Hello, C!The function does not care where the bytes come from. They may come from a string literal, an array, a slice, or allocated memory.
That is why []const u8 is the standard input type for text.
A String Literal Has a Sentinel
A string literal in Zig has a sentinel value at the end.
For example:
const name = "Zig";The visible text has 3 bytes:
Z i gBut Zig also stores a zero byte after them:
Z i g 0This final zero is called a sentinel.
It is useful for C interop because C strings usually end with a zero byte.
The visible length is still 3:
const name = "Zig";
std.debug.print("{}\n", .{name.len});Output:
3The sentinel is not counted in .len.
For beginners, remember this:
A string literal behaves like a read-only byte slice, but it also has a hidden zero byte after the visible text.
Escape Sequences
Some characters are written with escape sequences.
A newline is written as:
"\n"Example:
std.debug.print("line one\nline two\n", .{});Output:
line one
line twoA tab is written as:
"\t"Example:
std.debug.print("name\tage\n", .{});A double quote inside a string is written as:
"She said \"hello\""A backslash is written as:
"C:\\Users\\zig"Common escapes:
| Escape | Meaning |
|---|---|
\n | newline |
\t | tab |
\" | double quote |
\\ | backslash |
\r | carriage return |
\0 | zero byte |
Multiline String Literals
Zig has multiline string literals.
They begin each line with two backslashes:
const text =
\\line one
\\line two
\\line three
;This is useful for long text, generated code, help messages, SQL, JSON examples, and test data.
Example:
const std = @import("std");
pub fn main() void {
const help =
\\usage: demo [options]
\\
\\options:
\\ --help show help
\\ --version show version
;
std.debug.print("{s}\n", .{help});
}Output:
usage: demo [options]
options:
--help show help
--version show versionThe multiline form avoids escaping every quote and newline manually.
Strings Are UTF-8 Bytes
Zig string literals are UTF-8.
That means non-ASCII text is stored as one or more bytes per character.
Example:
const text = "é";The character é is one visible character, but in UTF-8 it uses 2 bytes.
So this:
std.debug.print("{}\n", .{text.len});prints:
2Another example:
const text = "你好";Each Chinese character uses 3 bytes in UTF-8, so the length is 6 bytes.
std.debug.print("{}\n", .{text.len});Output:
6This is important.
In Zig, .len on text usually means byte length, not character count.
Iterating Over Bytes
When you loop over a string, you get bytes.
const std = @import("std");
pub fn main() void {
const text = "Zig";
for (text) |byte| {
std.debug.print("{}\n", .{byte});
}
}Output:
90
105
103Those are byte values.
You can print them as characters:
for (text) |byte| {
std.debug.print("{c}\n", .{byte});
}Output:
Z
i
gThis works well for ASCII text.
For full Unicode text, one visible character may use multiple bytes. You should not assume one byte equals one character.
Comparing Strings
Use the standard library to compare strings.
const std = @import("std");
pub fn main() void {
const a = "zig";
const b = "zig";
const c = "Zig";
std.debug.print("{}\n", .{std.mem.eql(u8, a, b)});
std.debug.print("{}\n", .{std.mem.eql(u8, a, c)});
}Output:
true
falseThis function compares byte sequences.
std.mem.eql(u8, a, b)means:
compare two slices of u8 values for equalityDo not compare strings by comparing pointers. You usually want to compare contents, not addresses.
Finding Text
The standard library has functions for searching byte slices.
Example:
const std = @import("std");
pub fn main() void {
const text = "hello zig";
if (std.mem.indexOf(u8, text, "zig")) |index| {
std.debug.print("found at {}\n", .{index});
} else {
std.debug.print("not found\n", .{});
}
}Output:
found at 6The result is optional. If the search succeeds, you get an index. If it fails, you get null.
Slicing Strings
Since strings behave like byte slices, you can slice them.
const text = "hello zig";
const first = text[0..5];
const second = text[6..9];Now:
first = hello
second = zigExample:
const std = @import("std");
pub fn main() void {
const text = "hello zig";
std.debug.print("{s}\n", .{text[0..5]});
std.debug.print("{s}\n", .{text[6..9]});
}Output:
hello
zigBe careful with UTF-8. Slicing at arbitrary byte positions can split a character.
For ASCII text, byte indexes and character positions match. For non-ASCII text, they may not.
Mutable Text
A string literal cannot be changed, but a byte array can.
var text = [_]u8{ 'z', 'i', 'g' };
text[0] = 'b';Now it contains:
b i gYou can print it as a string by slicing the array:
std.debug.print("{s}\n", .{text[0..]});Complete example:
const std = @import("std");
pub fn main() void {
var text = [_]u8{ 'z', 'i', 'g' };
text[0] = 'b';
std.debug.print("{s}\n", .{text[0..]});
}Output:
bigUse mutable byte arrays or allocated buffers when text must change.
Building Text with a Buffer
A common Zig pattern is: give a function a buffer, and let it write text into that buffer.
const std = @import("std");
pub fn main() !void {
var buffer: [64]u8 = undefined;
const name = "Zig";
const message = try std.fmt.bufPrint(buffer[0..], "Hello, {s}!", .{name});
std.debug.print("{s}\n", .{message});
}Output:
Hello, Zig!Here:
var buffer: [64]u8 = undefined;creates a fixed array of 64 bytes.
This line:
const message = try std.fmt.bufPrint(buffer[0..], "Hello, {s}!", .{name});writes formatted text into the buffer and returns a slice containing the part that was used.
The returned message points into buffer. It does not allocate.
Building Text with an Allocator
When the output size is not known in advance, you can allocate.
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const name = "Zig";
const message = try std.fmt.allocPrint(allocator, "Hello, {s}!", .{name});
defer allocator.free(message);
std.debug.print("{s}\n", .{message});
}This creates a new allocated byte slice.
Because it allocates, you must free it:
defer allocator.free(message);This pattern is common:
bufPrint: writes into caller-provided memory
allocPrint: allocates new memoryUse bufPrint when you already have a buffer. Use allocPrint when you need the function to create the result.
Common Mistake: Expecting .len to Count Characters
This surprises many beginners:
const text = "é";
std.debug.print("{}\n", .{text.len});Output:
2The visible character count is 1. The byte count is 2.
Zig reports the byte count.
For ASCII text, byte count and visible character count are usually the same. For Unicode text, they often differ.
Common Mistake: Modifying a String Literal
This is wrong:
const text = "hello";
text[0] = 'H';String literals are read-only.
Use a mutable array:
var text = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
text[0] = 'H';Or allocate mutable memory if the size is dynamic.
Common Mistake: Using {} Instead of {s}
This is usually wrong for printing strings:
std.debug.print("{}\n", .{"hello"});Use {s}:
std.debug.print("{s}\n", .{"hello"});The {s} formatter means “print this byte slice as a string.”
Use {} for many ordinary values, such as integers and booleans. Use {s} for strings.
Complete Example
const std = @import("std");
fn startsWithHello(text: []const u8) bool {
return std.mem.startsWith(u8, text, "hello");
}
pub fn main() !void {
const a = "hello zig";
const b = "goodbye zig";
std.debug.print("{s}: {}\n", .{ a, startsWithHello(a) });
std.debug.print("{s}: {}\n", .{ b, startsWithHello(b) });
var buffer: [64]u8 = undefined;
const message = try std.fmt.bufPrint(buffer[0..], "language = {s}", .{"Zig"});
std.debug.print("{s}\n", .{message});
}Output:
hello zig: true
goodbye zig: false
language = ZigThis example shows the main string literal habits:
const a = "hello zig";A string literal is read-only text.
fn startsWithHello(text: []const u8) boolA function accepts text as []const u8.
std.mem.startsWith(u8, text, "hello")String operations usually work on byte slices.
std.fmt.bufPrint(buffer[0..], "language = {s}", .{"Zig"})Formatted text can be written into a caller-provided buffer.
Summary
A string literal is read-only UTF-8 text stored in the program.
Most Zig APIs represent text as:
[]const u8That means a read-only slice of bytes.
String length is byte length, not character count.
String literals cannot be modified. Use a mutable byte array or allocated buffer when you need editable text.
Use {s} to print strings. Use std.mem functions to compare, search, and inspect text.
Zig treats text honestly: it is bytes. That may feel low-level at first, but it gives you clear control over memory, encoding, and allocation.