Skip to content

The Structure of a Function

Functions are reusable blocks of code.

Defining Functions

Functions are reusable blocks of code.

Instead of writing the same logic again and again, you place the logic inside a function and call it whenever you need it.

In Zig, functions are one of the most important building blocks of a program. Every Zig program starts from a function called main.

A function can:

  • receive input
  • perform work
  • return a result

A simple function looks like this:

fn sayHello() void {
    std.debug.print("Hello!\n", .{});
}

This defines a function named sayHello.

Let us break it apart carefully.

The Structure of a Function

fn sayHello() void {
    std.debug.print("Hello!\n", .{});
}

Each piece has meaning.

PartMeaning
fndeclares a function
sayHellofunction name
()parameter list
voidreturn type
{}function body

The body contains the code that runs when the function is called.

Calling a Function

Defining a function does not run it.

You must call it.

const std = @import("std");

fn sayHello() void {
    std.debug.print("Hello!\n", .{});
}

pub fn main() void {
    sayHello();
}

Output:

Hello!

The line:

sayHello();

calls the function.

Execution jumps into the function body, runs the code, then returns back to the caller.

Why Functions Matter

Functions help organize programs.

Without functions:

pub fn main() void {
    std.debug.print("Loading file...\n", .{});
    std.debug.print("Parsing file...\n", .{});
    std.debug.print("Saving result...\n", .{});
}

As programs grow, everything becomes mixed together.

With functions:

const std = @import("std");

fn loadFile() void {
    std.debug.print("Loading file...\n", .{});
}

fn parseFile() void {
    std.debug.print("Parsing file...\n", .{});
}

fn saveResult() void {
    std.debug.print("Saving result...\n", .{});
}

pub fn main() void {
    loadFile();
    parseFile();
    saveResult();
}

Now the program is easier to read.

The main function describes the high-level flow of the program.

Function Names

Function names should describe what the function does.

Good names:

readFile()
parseJson()
calculateTotal()
sendRequest()

Bad names:

doThing()
stuff()
x()
abc()

A good function name reduces mental effort when reading code.

Zig commonly uses camelCase for functions.

Functions Without Return Values

Some functions only perform actions.

Example:

fn printBanner() void {
    std.debug.print("=== APP START ===\n", .{});
}

The return type is void.

void means “no value is returned.”

Functions That Return Values

Functions can also produce values.

Example:

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

This function:

  • receives two integers
  • adds them
  • returns the result

The return type is:

i32

meaning a signed 32-bit integer.

Using the function:

const std = @import("std");

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

pub fn main() void {
    const result = add(10, 20);

    std.debug.print("{}\n", .{result});
}

Output:

30

Understanding return

The return keyword immediately exits the function.

Example:

fn square(x: i32) i32 {
    return x * x;
}

Once return runs, the function ends.

Any code after it does not execute.

fn test() i32 {
    return 5;

    // never runs
    return 10;
}

Zig detects unreachable code in many situations.

Parameters

Parameters are inputs to a function.

Example:

fn greet(name: []const u8) void {
    std.debug.print("Hello, {s}!\n", .{name});
}

Here:

name: []const u8

means:

  • parameter name: name
  • parameter type: string slice

Calling it:

greet("Alice");
greet("Bob");

Output:

Hello, Alice!
Hello, Bob!

Functions become flexible because parameters allow different input values.

Multiple Parameters

Functions can accept many parameters.

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

Calling:

const value = multiply(2, 3, 4);

Result:

24

Parameters are separated by commas.

Type Checking

Zig checks parameter types strictly.

Example:

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

This works:

add(1, 2);

This fails:

add("hello", "world");

The compiler stops with an error because strings are not integers.

This strictness helps prevent bugs.

Function Signatures

A function signature describes:

  • function name
  • parameter types
  • return type

Example:

fn divide(a: f64, b: f64) f64

Meaning:

  • function name: divide
  • parameters: two f64
  • returns: one f64

When programmers discuss APIs, they often talk about function signatures.

Public Functions

Earlier we saw:

pub fn main() void

The word pub means public.

Without pub, a function is private to the current file.

Example:

fn helper() void {}

Private function.

pub fn api() void {}

Public function.

Most helper functions should remain private unless other files need access.

Functions as Building Blocks

Large programs are built from many small functions.

Instead of writing one giant function:

pub fn main() void {
    // 5000 lines
}

good programs divide work into smaller pieces:

loadConfig();
connectDatabase();
startServer();

Small functions are:

  • easier to test
  • easier to debug
  • easier to reuse
  • easier to understand

A Complete Example

const std = @import("std");

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

fn printResult(value: i32) void {
    std.debug.print("Result: {}\n", .{value});
}

pub fn main() void {
    const result = add(5, 7);

    printResult(result);
}

Output:

Result: 12

This program already demonstrates an important idea:

  • one function computes
  • another function prints
  • main coordinates everything

Each function has a clear responsibility.

Mental Model

You can think of a function like a machine.

Input goes in:

a, b

work happens inside:

a + b

output comes out:

result

Functions let you package logic into reusable units.

This is one of the foundations of programming, not just in Zig, but in nearly every programming language.