Zig: Const

Home · Blog

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.

Declaring variables

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:

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.

Pointer to const

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.

Parameters and captures

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.