24 February 2025
When I first started learning Zig, I found the const
keyword a bit confusing. It seems simple – const
is for
constants – but the keyword is actually used for two different but
related purposes, and some of the details are a bit surprising. In this
article I’ll try to explain exactly how it works.
One disclaimer: I’ve tested everything here with Zig 0.13. If you’re using a later version then there’s a good chance some of the details have changed.
If you declare a variable with const
instead of
var
, then you can’t modify it:
var a: u64 = 1;
a += 1; // fine
const b: u64 = 2;
b += 1; // not allowed
If the variable has a pointer or slice type, then declaring it
const
does not apply to the values it points to:
const ptr = &a;
ptr.* = 5; // fine
ptr = &b; // error
Declaring constants const
is usually a good idea because
it makes it clear how the variable will be used. In fact, for simple
cases the Zig compiler will force you to use const
if a
variable is never modified. For example, this code will not compile:
fn feet_to_meters(ft: f64) f64 {
var factor = 0.3048; // error: local variable is never mutated
return ft * factor;
}
However, there are some corner cases where the compiler will allow
const
even if a variable is never modified:
&
)
is applied to the variable at some point.It’s probably a good idea to still use const
where
possible in those cases, to make the code clearer and also because this
behavior might change in the future.
The second use of const
is for pointer types. In
general, you can have mutable pointers, which let you modify the data
they point to, and const pointers, which don’t:
var a: u64 = 1;
const mutable_pointer: *u64 = &a;
mutable_pointer.* = 2; // fine
const const_pointer: *const u64 = &a;
const_pointer.* = 3; // not allowed
The const
in the pointer type is unrelated to whether
you declare the pointer variable with const
or
var
. A mutable pointer lets you modify the value it points
to; a pointer declared with val
lets you change
which value it points to.
Slices can also be mutable or const:
var array = [_]u64{ 1, 2, 3, 4, 5 };
const mutable_slice: []u64 = &array;
mutable_slice[0] = 6; // fine
const const_slice: []const u64 = &array;
const_slice[0] = 7; // not allowed
When you take the address of a variable declared with
const
, you get a const pointer – otherwise you’d be able to
modify the constant through that pointer. In the same way, a slice (or
many-item pointer) that refers to an array declared with
const
has to be a const slice (or const many-item pointer).
That also applies to string literals, since those are implemented as
const arrays:
const greeting = "hello";
const slice: []u8 = &greeting; // not allowed
slice[1] = 'u'; // this would modify read-only memory
Function pointers always have a const pointer type:
const fn_ptr: *const fn () void = some_function;
You can’t modify functions at runtime, so a mutable function pointer wouldn’t make sense in Zig.
Two places where you can’t mark variables as const are function parameters and variables introduced by a for loop:
fn f(const x: u64) void { // not possible
for (array) |const x| { // nope
The reason is simple: These variables are always immutable in Zig.
Coming from Go, I would have expected at least function parameters to be mutable. In Go, function arguments are always passed as copies, so why shouldn’t you be allowed to mess with them? On the other hand… Even in Go, it’s quite rare for programmers to take advantage of parameters’ being mutable, so that can be a bit of a surprise when you’re reading code. In the interest of making code easy to read it’s probably best to avoid this kind of surprise. Making parameters immutable also means the compiler can pass large stucts or arrays as references instead of having to make a copy.