A function in Zig is a named block of code with explicit parameters and return type. So far we’ve written almost everything inside main, but to write maintainable software we need modularity.
Functions in Zig are the primary mechanism for breaking complex problems into small, reusable tasks.
Although the syntax will feel familiar if you come from C, Go, or Rust, Zig introduces an important idea regarding parameters: their value cannot be reassigned inside the function.
Anatomy of a Function
The declaration of a function in Zig follows a very clean structure. We read from left to right: keyword, name, parameters, and return type.
fn functionName(param1: Type, param2: Type) ReturnType {
// Function body
return value;
}Let’s look at a real example of a function that adds two 32-bit integers:
fn add(a: i32, b: i32) i32 {
const result = a + b;
return result;
}fn: Indicates we are defining a function (as opposed tovarorconst).add: The name. By convention, functions in Zig use camelCase.(a: i32, b: i32): The parameter list. It is mandatory to specify the type of each one.i32(at the end): The data type the function promises to return.return: Returns the value and terminates the function’s execution.
If the function returns nothing (it is a procedure), the return type must be explicitly void.
Visibility with pub
By default, a function is not accessible to modules that import the file where it is defined. To be part of the module’s public interface, it must be declared with pub.
If we want a function to be accessible from other files (or to be the entry point like main), we must use the pub keyword.
// Only visible in this file
fn privateFunction() void { ... }
// Visible from any file that imports this module
pub fn publicFunction() void { ... }Parameters are Immutable
This is where Zig drastically differs from C or C++.
In C, when you pass a value to a function, a local copy is created. You can modify that copy inside the function without affecting the outside.
// C (Valid)
void counter(int x) {
x = x + 1; // We modify the local copy of x
}In Zig, this is prohibited. Function parameters are considered constants (const).
// Zig (COMPILATION ERROR)
fn tryToModify(x: i32) void {
x = x + 1; // error: cannot assign to constant
}Why this restriction?
Zig wants data flow to be clear. The function cannot reassign the parameter, although it can modify the pointed-to memory if it receives a mutable pointer or a slice []T. Immutability applies to the parameter’s value, not necessarily to the data it references.
How do we solve this?
If you need to modify a received value for internal calculations, simply create a mutable local copy using var:
fn calculate(input: i32) i32 {
var mutable_copy = input;
mutable_copy += 10;
return mutable_copy;
}Return Values and Discarding
Zig is very strict with data. If a function returns a value, you are obligated to use it.
fn getNumber() i32 {
return 5;
}
pub fn main() void {
getNumber(); // Error: value of type 'i32' ignored
}This prevents bugs where we call functions that return error codes or important states and accidentally ignore them.
If you explicitly want to ignore the result (because you only care about the function’s side effects), you must assign it to the discard variable _:
pub fn main() void {
_ = getNumber(); // Correct, we "throw away" the 5
}Recursion
Zig supports recursion (a function that calls itself), but we must use it carefully. Each call consumes stack space, and Zig 0.16 does not protect the program against stack overflow.
fn factorial(n: u64) u64 {
if (n == 0) return 1;
return n * factorial(n - 1);
}In systems and embedded programming, recursion is often converted into loops (while or for) to guarantee predictable memory usage and avoid blowing the stack.
Pass by Value or by Reference
A common doubt is: Does Zig pass data by value (copying) or by reference (pointer)?
The short answer is: you define the semantics, and the compiler decides the most efficient representation.
- For primitive types (
i32,bool,f64), they are passed by copy. - For
structs, unions, and large arrays, Zig may pass them internally by reference if it is more efficient, but the function retains immutable value semantics.
If you want to modify the original value, you must express it through a pointer or a mutable slice. We will see both cases in the pointers and memory chapter.