Zig Control Flow
Control flow is a core concept in programming. This chapter will cover various control flow statements in Zig in detail.
if Statements
Basic if Statement
The if statement is used for conditional branching:
const std = @import("std");
pub fn main() void {
const age = 18;
if (age >= 18) {
std.debug.print("You are an adult\n", .{});
}
const temperature = 25;
if (temperature > 30) {
std.debug.print("It's hot\n", .{});
} else {
std.debug.print("The weather is nice\n", .{});
}
}if-else if Chain
const std = @import("std");
pub fn main() void {
const score = 85;
if (score >= 90) {
std.debug.print("Excellent\n", .{});
} else if (score >= 80) {
std.debug.print("Good\n", .{});
} else if (score >= 70) {
std.debug.print("Average\n", .{});
} else if (score >= 60) {
std.debug.print("Pass\n", .{});
} else {
std.debug.print("Fail\n", .{});
}
}if Expression
In Zig, if is an expression and can return a value:
const std = @import("std");
pub fn main() void {
const x = 10;
const y = 20;
// if expression
const max = if (x > y) x else y;
std.debug.print("Maximum: {}\n", .{max});
// Complex if expression
const status = if (x > 0)
"positive"
else if (x < 0)
"negative"
else
"zero";
std.debug.print("Number status: {s}\n", .{status});
}if with Optional Values
if can be used to handle optional values:
const std = @import("std");
pub fn main() void {
var maybe_number: ?i32 = 42;
// Unwrap optional value
if (maybe_number) |number| {
std.debug.print("The number is: {}\n", .{number});
} else {
std.debug.print("No number\n", .{});
}
// Change to null and test again
maybe_number = null;
if (maybe_number) |number| {
std.debug.print("The number is: {}\n", .{number});
} else {
std.debug.print("Now there's no number\n", .{});
}
}if with Error Union Types
const std = @import("std");
const ParseError = error{
InvalidFormat,
OutOfRange,
};
fn parseNumber(input: []const u8) ParseError!i32 {
if (std.mem.eql(u8, input, "42")) {
return 42;
} else if (std.mem.eql(u8, input, "100")) {
return 100;
} else if (std.mem.eql(u8, input, "999")) {
return ParseError.OutOfRange;
} else {
return ParseError.InvalidFormat;
}
}
pub fn main() void {
const inputs = [_][]const u8{ "42", "100", "999", "abc" };
for (inputs) |input| {
if (parseNumber(input)) |number| {
std.debug.print("Parse success: {} -> {}\n", .{ input, number });
} else |err| {
std.debug.print("Parse failed: {} -> {}\n", .{ input, err });
}
}
}switch Statements
Basic switch Statement
The switch statement is used for multi-way branching:
const std = @import("std");
pub fn main() void {
const day = 3;
switch (day) {
1 => std.debug.print("Monday\n", .{}),
2 => std.debug.print("Tuesday\n", .{}),
3 => std.debug.print("Wednesday\n", .{}),
4 => std.debug.print("Thursday\n", .{}),
5 => std.debug.print("Friday\n", .{}),
6, 7 => std.debug.print("Weekend\n", .{}),
else => std.debug.print("Invalid day\n", .{}),
}
}switch Expression
switch can also be used as an expression:
const std = @import("std");
pub fn main() void {
const grade = 'B';
const score = switch (grade) {
'A' => 90,
'B' => 80,
'C' => 70,
'D' => 60,
'F' => 0,
else => -1,
};
std.debug.print("Grade {} corresponds to score: {}\n", .{ grade, score });
}Range Matching
switch supports range matching:
const std = @import("std");
pub fn main() void {
const number = 75;
const category = switch (number) {
0...59 => "Fail",
60...69 => "Pass",
70...79 => "Average",
80...89 => "Good",
90...100 => "Excellent",
else => "Invalid score",
};
std.debug.print("Score {} is: {s}\n", .{ number, category });
}switch with Enums
switch works very well with enums:
const std = @import("std");
const Color = enum {
Red,
Green,
Blue,
Yellow,
};
fn getColorName(color: Color) []const u8 {
return switch (color) {
.Red => "Red",
.Green => "Green",
.Blue => "Blue",
.Yellow => "Yellow",
};
}
pub fn main() void {
const colors = [_]Color{ .Red, .Green, .Blue, .Yellow };
for (colors) |color| {
std.debug.print("Color: {s}\n", .{getColorName(color)});
}
}switch with Union Types
switch can handle union types:
const std = @import("std");
const Value = union(enum) {
Integer: i32,
Float: f64,
String: []const u8,
Boolean: bool,
};
fn processValue(value: Value) void {
switch (value) {
.Integer => |int| std.debug.print("Integer: {}\n", .{int}),
.Float => |float| std.debug.print("Float: {d:.2}\n", .{float}),
.String => |string| std.debug.print("String: {s}\n", .{string}),
.Boolean => |boolean| std.debug.print("Boolean: {}\n", .{boolean}),
}
}
pub fn main() void {
const values = [_]Value{
Value{ .Integer = 42 },
Value{ .Float = 3.14 },
Value{ .String = "Hello" },
Value{ .Boolean = true },
};
for (values) |value| {
processValue(value);
}
}Complex switch Patterns
const std = @import("std");
const Point = struct {
x: i32,
y: i32,
};
fn analyzePoint(point: Point) []const u8 {
return switch (point) {
.{ .x = 0, .y = 0 } => "Origin",
.{ .x = 0, .y = _ } => "On Y-axis",
.{ .x = _, .y = 0 } => "On X-axis",
.{ .x = var x, .y = var y } => blk: {
if (x == y) {
break :blk "On diagonal";
} else if (x > 0 and y > 0) {
break :blk "First quadrant";
} else if (x < 0 and y > 0) {
break :blk "Second quadrant";
} else if (x < 0 and y < 0) {
break :blk "Third quadrant";
} else {
break :blk "Fourth quadrant";
}
},
};
}
pub fn main() void {
const points = [_]Point{
.{ .x = 0, .y = 0 },
.{ .x = 0, .y = 5 },
.{ .x = 3, .y = 0 },
.{ .x = 2, .y = 2 },
.{ .x = 3, .y = 4 },
.{ .x = -2, .y = 3 },
.{ .x = -1, .y = -1 },
.{ .x = 2, .y = -3 },
};
for (points) |point| {
std.debug.print("Point ({}, {}) is at: {s}\n",
.{ point.x, point.y, analyzePoint(point) });
}
}defer Statement
Basic defer
The defer statement executes when the current scope ends:
const std = @import("std");
pub fn main() void {
std.debug.print("Start execution\n", .{});
defer std.debug.print("This executes at function end\n", .{});
std.debug.print("Middle execution\n", .{});
// defer statement executes when function ends
}Multiple defer Statements
Multiple defer statements execute in LIFO (Last In, First Out) order:
const std = @import("std");
pub fn main() void {
std.debug.print("Start\n", .{});
defer std.debug.print("First defer\n", .{});
defer std.debug.print("Second defer\n", .{});
defer std.debug.print("Third defer\n", .{});
std.debug.print("Middle\n", .{});
// Execution order: Third defer -> Second defer -> First defer
}Practical defer Usage
defer is commonly used for resource cleanup:
const std = @import("std");
fn processFile(filename: []const u8) !void {
std.debug.print("Opening file: {s}\n", .{filename});
// Simulate file operation
defer std.debug.print("Closing file: {s}\n", .{filename});
std.debug.print("Processing file content\n", .{});
// defer executes even if an error occurs
if (std.mem.eql(u8, filename, "error.txt")) {
return error.FileError;
}
std.debug.print("File processing complete\n", .{});
}
pub fn main() void {
processFile("normal.txt") catch |err| {
std.debug.print("Error processing file: {}\n", .{err});
};
std.debug.print("---\n", .{});
processFile("error.txt") catch |err| {
std.debug.print("Error processing file: {}\n", .{err});
};
}errdefer Statement
Basic errdefer
errdefer only executes when the function returns an error:
const std = @import("std");
const ProcessError = error{
InitializationFailed,
ProcessingFailed,
};
fn processData(should_fail: bool) ProcessError!void {
std.debug.print("Starting initialization\n", .{});
errdefer std.debug.print("Initialization failed, performing cleanup\n", .{});
if (should_fail) {
return ProcessError.InitializationFailed;
}
std.debug.print("Initialization successful\n", .{});
std.debug.print("Processing data\n", .{});
}
pub fn main() void {
std.debug.print("=== Success case ===\n", .{});
processData(false) catch |err| {
std.debug.print("Error: {}\n", .{err});
};
std.debug.print("\n=== Failure case ===\n", .{});
processData(true) catch |err| {
std.debug.print("Error: {}\n", .{err});
};
}Combining errdefer and defer
const std = @import("std");
const ResourceError = error{
AllocationFailed,
InitializationFailed,
};
fn allocateAndInitialize(should_fail_alloc: bool, should_fail_init: bool) ResourceError!void {
std.debug.print("Starting resource allocation\n", .{});
if (should_fail_alloc) {
return ResourceError.AllocationFailed;
}
// Resource allocation successful
defer std.debug.print("Releasing resource\n", .{});
errdefer std.debug.print("Allocation succeeded but initialization failed, releasing resource\n", .{});
std.debug.print("Starting initialization\n", .{});
if (should_fail_init) {
return ResourceError.InitializationFailed;
}
std.debug.print("Initialization successful\n", .{});
}
pub fn main() void {
std.debug.print("=== Allocation failure ===\n", .{});
allocateAndInitialize(true, false) catch |err| {
std.debug.print("Error: {}\n", .{err});
};
std.debug.print("\n=== Initialization failure ===\n", .{});
allocateAndInitialize(false, true) catch |err| {
std.debug.print("Error: {}\n", .{err});
};
std.debug.print("\n=== All successful ===\n", .{});
allocateAndInitialize(false, false) catch |err| {
std.debug.print("Error: {}\n", .{err});
};
}Block Expressions
Basic Block Expression
Blocks can be used as expressions:
const std = @import("std");
pub fn main() void {
const result = blk: {
const x = 10;
const y = 20;
break :blk x + y;
};
std.debug.print("Block expression result: {}\n", .{result});
}Complex Block Expressions
const std = @import("std");
fn calculateGrade(score: i32) []const u8 {
return blk: {
if (score >= 90) {
break :blk "A";
} else if (score >= 80) {
break :blk "B";
} else if (score >= 70) {
break :blk "C";
} else if (score >= 60) {
break :blk "D";
} else {
break :blk "F";
}
};
}
pub fn main() void {
const scores = [_]i32{ 95, 85, 75, 65, 55 };
for (scores) |score| {
const grade = calculateGrade(score);
std.debug.print("Score {} corresponds to grade: {s}\n", .{ score, grade });
}
}Conditional Compilation
comptime if
Use comptime if for conditional compilation:
const std = @import("std");
const builtin = @import("builtin");
pub fn main() void {
comptime {
if (builtin.os.tag == .windows) {
std.debug.print("Compilation target is Windows\n", .{});
} else if (builtin.os.tag == .linux) {
std.debug.print("Compilation target is Linux\n", .{});
} else if (builtin.os.tag == .macos) {
std.debug.print("Compilation target is macOS\n", .{});
} else {
std.debug.print("Compilation target is other system\n", .{});
}
}
const is_debug = comptime builtin.mode == .Debug;
if (comptime is_debug) {
std.debug.print("This is a debug build\n", .{});
} else {
std.debug.print("This is a release build\n", .{});
}
}Compile-time switch
const std = @import("std");
fn getOptimizationLevel(comptime mode: std.builtin.Mode) []const u8 {
return comptime switch (mode) {
.Debug => "No optimization",
.ReleaseSafe => "Safe optimization",
.ReleaseFast => "Speed optimization",
.ReleaseSmall => "Size optimization",
};
}
pub fn main() void {
const optimization = comptime getOptimizationLevel(@import("builtin").mode);
std.debug.print("Current optimization level: {s}\n", .{optimization});
}Control Flow Best Practices
1. Keep Conditions Simple
const std = @import("std");
pub fn main() void {
const user_age = 25;
const has_license = true;
const has_insurance = true;
// ✅ Good practice: clear condition
const can_drive = user_age >= 18 and has_license and has_insurance;
if (can_drive) {
std.debug.print("Can drive\n", .{});
} else {
std.debug.print("Cannot drive\n", .{});
}
}2. Prefer switch Over Long if-else Chains
const std = @import("std");
const Status = enum {
Pending,
Processing,
Completed,
Failed,
};
pub fn main() void {
const status = Status.Processing;
// ✅ Good practice: use switch
const message = switch (status) {
.Pending => "Pending",
.Processing => "Processing",
.Completed => "Completed",
.Failed => "Failed",
};
std.debug.print("Status: {s}\n", .{message});
}3. Use defer for Resource Management
const std = @import("std");
fn processWithCleanup() !void {
std.debug.print("Acquiring resource\n", .{});
// ✅ Good practice: set cleanup code immediately
defer std.debug.print("Releasing resource\n", .{});
std.debug.print("Using resource\n", .{});
// Resource will be released regardless of how function exits
}
pub fn main() void {
processWithCleanup() catch {};
}Practice Exercises
Exercise 1: Grade Management System
const std = @import("std");
const Student = struct {
name: []const u8,
score: i32,
};
pub fn main() void {
const students = [_]Student{
.{ .name = "Alice", .score = 85 },
.{ .name = "Bob", .score = 92 },
.{ .name = "Charlie", .score = 78 },
.{ .name = "David", .score = 65 },
};
// TODO: Use control flow statements to complete the following tasks:
// 1. Assign grades to each student (A: 90+, B: 80-89, C: 70-79, D: 60-69, F: <60)
// 2. Count the number of students in each grade
// 3. Find the students with highest and lowest scores
}Exercise 2: Simple Calculator
const std = @import("std");
const Operation = enum {
Add,
Subtract,
Multiply,
Divide,
};
pub fn main() void {
// TODO: Implement a simple calculator
// 1. Use switch to handle different operations
// 2. Handle division by zero error
// 3. Use defer for resource cleanup
}Exercise 3: State Machine
const std = @import("std");
const State = enum {
Idle,
Running,
Paused,
Stopped,
};
const Event = enum {
Start,
Pause,
Resume,
Stop,
};
pub fn main() void {
// TODO: Implement a simple state machine
// 1. Based on current state and event, determine next state
// 2. Use switch to handle state transitions
// 3. Handle invalid state transitions
}Summary
This chapter covered Zig control flow statements in detail:
- ✅
ifstatements: conditional branching, supports expression form - ✅
switchstatements: multi-way branching, powerful features - ✅
deferanderrdefer: resource management and cleanup - ✅ Block expressions: using code blocks as expressions
- ✅ Conditional compilation: compile-time control flow
- ✅ Best practices and practical applications
Mastering control flow is fundamental to writing complex program logic. In the next chapter, we'll learn about Zig's error handling mechanisms.