Skip to main content

Function Declaration

Functions in Zig are declared with the fn keyword:
const std = @import("std");
const expect = std.testing.expect;

// Functions are declared like this
fn add(a: i8, b: i8) i8 {
    if (a == 0) {
        return b;
    }
    return a + b;
}

test "function" {
    try expect(add(5, 6) == 11);
}
Function parameters are immutable by default. To modify a parameter, pass it as a pointer.

Parameters and Return Values

Basic Parameters

fn multiply(a: i32, b: i32) i32 {
    return a * b;
}

Multiple Return Values

Zig doesn’t have built-in tuple returns, but you can return structs:
const Result = struct {
    quotient: i32,
    remainder: i32,
};

fn divMod(a: i32, b: i32) Result {
    return .{
        .quotient = @divTrunc(a, b),
        .remainder = @rem(a, b),
    };
}

Void Returns

Functions that don’t return a value use void:
fn printMessage(msg: []const u8) void {
    std.debug.print("{s}\n", .{msg});
}

Function Visibility

Public Functions

Use pub to make functions visible when importing:
// The pub specifier allows the function to be visible when importing.
// Another file can use @import and call sub2
pub fn sub2(a: i8, b: i8) i8 {
    return a - b;
}

Private Functions

Functions without pub are private to the file:
fn internalHelper() void {
    // Only visible within this file
}
pub fn publicFunction() void {
    // Accessible from other files
}

Calling Conventions

Export Functions

The export specifier makes a function externally visible with C ABI:
// The export specifier makes a function externally visible in the generated
// object file, and makes it use the C ABI.
export fn sub(a: i8, b: i8) i8 {
    return a - b;
}

Extern Functions

The extern specifier declares functions resolved at link time:
// The extern specifier is used to declare a function that will be resolved
// at link time, when linking statically, or at runtime, when linking
// dynamically.
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(.winapi) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;

Inline Functions

// The inline calling convention forces a function to be inlined at all call sites.
// If the function cannot be inlined, it is a compile-time error.
inline fn shiftLeftOne(a: u32) u32 {
    return a << 1;
}

Naked Functions

// The naked calling convention makes a function not have any function 
// prologue or epilogue. This can be useful when integrating with assembly.
fn _start() callconv(.naked) noreturn {
    abort();
}
Naked functions should only be used for low-level programming and assembly integration.

Function Pointers

Function pointers are prefixed with *const:
const Call2Op = *const fn (a: i8, b: i8) i8;

fn doOp(fnCall: Call2Op, op1: i8, op2: i8) i8 {
    return fnCall(op1, op2);
}

fn add(a: i8, b: i8) i8 {
    return a + b;
}

fn sub(a: i8, b: i8) i8 {
    return a - b;
}

test "function pointers" {
    try expect(doOp(add, 5, 6) == 11);
    try expect(doOp(sub, 5, 6) == -1);
}
Function pointers enable callbacks, strategy patterns, and other higher-order programming techniques.

Special Return Types

NoReturn

Functions that never return use noreturn:
fn abort() noreturn {
    @branchHint(.cold);
    while (true) {}
}

Error Unions

Functions can return error unions:
fn parseU64(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;
    for (buf) |c| {
        const digit = charToDigit(c);
        if (digit >= radix) {
            return error.InvalidChar;
        }
        // ... parsing logic
    }
    return x;
}

Generic Functions

Functions can be generic by accepting type parameters:
fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
    };
}

// Usage
var buffer: [10]i32 = undefined;
var list = List(i32){
    .items = &buffer,
    .len = 0,
};
Generic functions in Zig are evaluated at compile-time, providing zero-cost abstractions.

Method Syntax

Zig supports method-style calls through the first parameter:
const Vec3 = struct {
    x: f32,
    y: f32,
    z: f32,

    pub fn init(x: f32, y: f32, z: f32) Vec3 {
        return Vec3{
            .x = x,
            .y = y,
            .z = z,
        };
    }

    pub fn dot(self: Vec3, other: Vec3) f32 {
        return self.x * other.x + self.y * other.y + self.z * other.z;
    }
};

test "dot product" {
    const v1 = Vec3.init(1.0, 0.0, 0.0);
    const v2 = Vec3.init(0.0, 1.0, 0.0);
    
    // Method syntax
    try expect(v1.dot(v2) == 0.0);
    
    // Equivalent function call
    try expect(Vec3.dot(v1, v2) == 0.0);
}

Branch Hints

You can provide hints to the optimizer:
fn abort() noreturn {
    @branchHint(.cold); // Tell optimizer this is rarely executed
    while (true) {}
}
fn fastPath(condition: bool) void {
    if (condition) {
        @branchHint(.likely);
        // frequently executed code
    }
}

Build docs developers (and LLMs) love